
아이폰의 제스처 기능 처럼 안드로이드 기기는 항상 back버튼이 있다.
네비게이션 아이콘에 뒤로가기 처리를 해도 back버튼은 별도로 존재하기 때문에 back버튼에도 동일하게 처리를 해줘야 한다.
a2 = activity as ContentActivity
a2.onBackPressedDispatcher.addCallback{
a2.removeFragment(ContentFragmentName.ADD_CONTENT_FRAGMENT)
this.remove()
}
모든 화면에 대해서 back버튼을 눌렀을 때에도 제대로 처리가 되는지 확인해야 한다.

// Tools.kt
// ContentActivity의 Fragments
enum class ContentFragmentName(var str:String){
MAIN_FRAGMENT("mainFragment"),
ADD_CONTENT_FRAGMENT("addContentFragment"),
READ_CONTENT_FRAGMENT("readContentFragment"),
}
// ContentActivity.kt
// 이름으로 분기한다.
// Fragment의 객체를 생성하여 변수에 담아준다.
when(name){
// 게시글 목록 화면
ContentFragmentName.MAIN_FRAGMENT -> {
newFragment = MainFragment()
}
// 게시글 작성 화면
ContentFragmentName.ADD_CONTENT_FRAGMENT -> {
newFragment = AddContentFragment()
}
// 게시글 읽기 화면
ContentFragmentName.READ_CONTENT_FRAGMENT -> {
newFragment = ReadContentFragment()
}
}
// 툴바 셋팅
fun settingToolbarAddContent(){
fragmentAddContentBinding.apply {
toolbarAddContent.apply {
// 타이틀
title = "글 작성"
// Back
setNavigationIcon(R.drawable.arrow_back_24px)
setNavigationOnClickListener {
contentActivity.removeFragment(ContentFragmentName.ADD_CONTENT_FRAGMENT)
}
// 메뉴
inflateMenu(R.menu.menu_add_content)
setOnMenuItemClickListener {
when(it.itemId){
// 카메라
R.id.menuItemAddContentCamera -> {}
// 앨범
R.id.menuItemAddContentAlbum -> {}
// 초기화
R.id.menuItemAddContentReset -> {}
// 완료
R.id.menuItemAddContentDone -> {
// ReadContentFragment로 이동한다.
contentActivity.replaceFragment(ContentFragmentName.READ_CONTENT_FRAGMENT, true, true, null)
}
}
true
}
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
android:transitionGroup="true"
tools:context=".fragment.ReadContentFragment">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbarReadContent"
style="@style/Theme.AndroidProject4BoardApp.Toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="제목"
app:startIconDrawable="@drawable/subject_24px">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/textFieldReadContentSubject"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="false"
android:cursorVisible="false"
android:focusable="false"
android:inputType="text"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:hint="게시판 종류"
app:startIconDrawable="@drawable/warning_24px">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/textFieldReadContentType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="false"
android:cursorVisible="false"
android:focusable="false"
android:inputType="text"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:hint="작성자 닉네임"
app:startIconDrawable="@drawable/person_add_24px">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/textFieldReadContentNickName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="false"
android:cursorVisible="false"
android:focusable="false"
android:inputType="text"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:hint="작성날짜"
app:startIconDrawable="@drawable/calendar_month_24px">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/textFieldReadContentDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="false"
android:cursorVisible="false"
android:focusable="false"
android:inputType="text"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:hint="글 내용"
app:startIconDrawable="@drawable/description_24px">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/textFieldReadContentText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="false"
android:cursorVisible="false"
android:focusable="false"
android:inputType="text|textMultiLine"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</com.google.android.material.textfield.TextInputLayout>
<ImageView
android:id="@+id/imageViewReadContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:adjustViewBounds="true"
app:srcCompat="@drawable/panorama_24px" />
</LinearLayout>
</ScrollView>
</LinearLayout>

// ReadContentFragment.kt
class ReadContentFragment : Fragment() {
lateinit var fragmentReadContentBinding: FragmentReadContentBinding
lateinit var contentActivity: ContentActivity
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
fragmentReadContentBinding = FragmentReadContentBinding.inflate(inflater)
contentActivity = activity as ContentActivity
settingToolbar()
return fragmentReadContentBinding.root
}
fun settingToolbar(){
fragmentReadContentBinding.toolbarReadContent.apply {
// 타이틀
title = "글 읽기"
// Back
setNavigationIcon(R.drawable.arrow_back_24px)
}
}
}
// ReadContentFragment.kt
// 뒤로가기 처리
fun backProcess(){
contentActivity.removeFragment(ContentFragmentName.READ_CONTENT_FRAGMENT)
contentActivity.removeFragment(ContentFragmentName.ADD_CONTENT_FRAGMENT)
}
// ReadContentFragment.kt
// Back
setNavigationIcon(R.drawable.arrow_back_24px)
setNavigationOnClickListener {
backProcess()
}
// ReadContentFragment.kt
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
fragmentReadContentBinding = FragmentReadContentBinding.inflate(inflater)
contentActivity = activity as ContentActivity
settingToolbar()
settingBackButton()
return fragmentReadContentBinding.root
}
// Back button 눌렀을 때
fun settingBackButton(){
contentActivity.onBackPressedDispatcher.addCallback {
// 뒤로가기 처리 메서드 호출
backProcess()
// 뒤로가기 버튼의 콜백을 제거한다.
// 제거를 안하면 이후에도 back버튼을 눌렀을때 이 콜백 내용만 처리됨
remove()
}
}

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menuItemReadContentReply"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/description_24px"
android:title="댓글"
app:showAsAction="always" />
<item
android:id="@+id/menuItemReadContentModify"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/edit_24px"
android:title="수정"
app:showAsAction="always" />
<item
android:id="@+id/menuItemReadContentDelete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/delete_24px"
android:title="삭제"
app:showAsAction="always" />
</menu>

// ReadContentFragment.kt
// 툴바 설정
fun settingToolbar(){
fragmentReadContentBinding.apply {
toolbarReadContent.apply {
// 타이틀
title = "글 읽기"
// Back
setNavigationIcon(R.drawable.arrow_back_24px)
setNavigationOnClickListener {
backProcess()
}
// 메뉴
inflateMenu(R.menu.menu_read_content)
setOnMenuItemClickListener {
when (it.itemId) {
// 댓글
R.id.menuItemReadContentReply -> {
}
// 수정
R.id.menuItemReadContentModify -> {
}
// 삭제
R.id.menuItemReadContentDelete -> {
}
}
true
}
}
}
}


클래스 상속을 Fragment에서 BottomSheetDialogFragment로 변경한다.
// ReadContentBottomFragment.kt
class ReadContentBottomFragment : BottomSheetDialogFragment() {
lateinit var fragmentReadContentBottomBinding: FragmentReadContentBottomBinding
lateinit var contentActivity: ContentActivity
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
fragmentReadContentBottomBinding = FragmentReadContentBottomBinding.inflate(inflater)
contentActivity = activity as ContentActivity
return fragmentReadContentBottomBinding.root
}
}
// ReadContentFragment.kt
// 메뉴
inflateMenu(R.menu.menu_read_content)
setOnMenuItemClickListener {
when (it.itemId) {
// 댓글
R.id.menuItemReadContentReply -> {
// 댓글을 보여줄 BottomSheet를 띄워준다.
showReplyBottomSheet()
}
// 수정
R.id.menuItemReadContentModify -> {
}
// 삭제
R.id.menuItemReadContentDelete -> {
}
}
true
}
// 댓글을 보여줄 BottomSheet를 띄워준다.
fun showReplyBottomSheet(){
val readContentBottomFragment = ReadContentBottomFragment()
readContentBottomFragment.show(contentActivity.supportFragmentManager, "ReplyBottomSheet")
}

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
android:padding="20dp"
tools:context=".fragment.ReadContentBottomFragment">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:hint="댓글"
app:endIconMode="clear_text"
app:startIconDrawable="@drawable/warning_24px">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/textFieldAddContentReply"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewAddContentReply"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/textViewRowReplyText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TextView"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<TextView
android:id="@+id/textViewRowReplyNickName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="TextView" />
<TextView
android:id="@+id/textViewRowReplyDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="TextView" />
</LinearLayout>
<Button
android:id="@+id/buttonRowReplyDelete"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
app:icon="@drawable/delete_24px" />
</LinearLayout>

// ReadContentBottomFragment.kt
// 댓글 목록을 보여줄 RecyclerView의 어댑터
inner class BottomRecyclerViewAdapter : RecyclerView.Adapter<BottomRecyclerViewAdapter.BottomViewHolder>(){
inner class BottomViewHolder(rowReadContentReplyBinding: RowReadContentReplyBinding) : RecyclerView.ViewHolder(rowReadContentReplyBinding.root){
val rowReadContentReplyBinding : RowReadContentReplyBinding
init {
this.rowReadContentReplyBinding = rowReadContentReplyBinding
rowReadContentReplyBinding.root.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BottomViewHolder {
val rowReadContentReplyBinding = RowReadContentReplyBinding.inflate(layoutInflater)
val bottomViewHolder = BottomViewHolder(rowReadContentReplyBinding)
return bottomViewHolder
}
override fun getItemCount(): Int {
return 100
}
override fun onBindViewHolder(holder: BottomViewHolder, position: Int) {
holder.rowReadContentReplyBinding.textViewRowReplyText.text = "댓글입니다 $position"
holder.rowReadContentReplyBinding.textViewRowReplyNickName.text = " 작성자 $position"
holder.rowReadContentReplyBinding.textViewRowReplyDate.text = "2024-03-07"
}
}
// ReadContentBottomFragment.kt
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
fragmentReadContentBottomBinding = FragmentReadContentBottomBinding.inflate(inflater)
contentActivity = activity as ContentActivity
settingRecyclerViewAddContentReply()
return fragmentReadContentBottomBinding.root
}
// RecyclerView 구성 메서드
fun settingRecyclerViewAddContentReply(){
fragmentReadContentBottomBinding.apply {
recyclerViewAddContentReply.apply {
// 어댑터
adapter = BottomRecyclerViewAdapter()
// 레이아웃 매니저
layoutManager = LinearLayoutManager(contentActivity)
// 데코레이션
val deco = MaterialDividerItemDecoration(contentActivity, MaterialDividerItemDecoration.VERTICAL)
addItemDecoration(deco)
}
}
}

댓글 개수가 적을 경우에는 BottomSheet를 열었을때의 높이가 줄어든다.

높이를 일정하게 고정시키려면 코드로 설정이 필요하다.
ReadContentBottomFragment는 BottomSheetDialogFragment를 상속받고 있기 때문에
dialog가 만들어질때 자동으로 호출되는 onCreateDialog를 오버라이딩 해준다.
// ReadContentBottomFragment.kt
// 다이얼로그가 만들어질 때 자동으로 호출되는 메서드
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// 다이얼로그를 받는다.
val dialog = super.onCreateDialog(savedInstanceState)
return dialog
}
단말기 액정의 길이를 구하는 메서드를 작성한다.
// ReadContentBottomFragment.kt
// 사용자 단말기 액정의 길이를 구해 반환하는 메서드
fun getWindowHeight() : Int {
// 화면 크기 정보를 담을 배열 객체
val displayMetrics = DisplayMetrics()
// 액정의 가로/세로 길이 정보를 담아준다.
contentActivity.windowManager.defaultDisplay.getMetrics(displayMetrics)
// 세로 길이를 반환해준다.
return displayMetrics.heightPixels
}
단말기 액정의 길이의 85% 값을 구하는 메서드를 작성한다.
// ReadContentBottomFragment.kt
// BottomSheet의 높이를 구한다(화면 액정의 85% 크기)
fun getBottomSheetDialogHeight() : Int{
return (getWindowHeight() * 0.85).toInt()
}
BottomSheet의 높이를 설정해주는 메서드를 작성한다.
// ReadContentBottomFragment.kt
// BottomSheet의 높이를 설정해준다.
fun setBottomSheetHeight(bottomSheetDialog:BottomSheetDialog){
// BottomSheet의 기본 뷰 객체를 가져온다
val bottomSheet = bottomSheetDialog.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)!!
// BottomSheet 높이를 설정할 수 있는 객체를 생성한다.
val behavior = BottomSheetBehavior.from(bottomSheet)
// 높이를 설정한다.
val layoutParams = bottomSheet.layoutParams
layoutParams.height = getBottomSheetDialogHeight()
bottomSheet.layoutParams = layoutParams
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
다이얼로그가 보일 때 동작하는 리스너에 setBottomSheetHeight 메서드를 호출한다.
// ReadContentBottomFragment.kt
// 다이얼로그가 만들어질 때 자동으로 호출되는 메서드
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// 다이얼로그를 받는다.
val dialog = super.onCreateDialog(savedInstanceState)
// 다이얼로그가 보일 때 동작하는 리스너
dialog.setOnShowListener {
val bottomSheetDialog = it as BottomSheetDialog
// 높이를 설정한다.
setBottomSheetHeight(bottomSheetDialog)
}
return dialog
}
댓글 수가 적더라도 BottomSheet가 85%의 높이만큼 올라오는 것을 확인할 수 있다.


// Tools.kt
// ContentActivity의 Fragments
enum class ContentFragmentName(var str:String){
MAIN_FRAGMENT("mainFragment"),
ADD_CONTENT_FRAGMENT("addContentFragment"),
READ_CONTENT_FRAGMENT("readContentFragment"),
MODIFY_CONTENT_FRAGMENT("modifyContentFragment"),
}
// ContentActivity.kt
// 이름으로 분기한다.
// Fragment의 객체를 생성하여 변수에 담아준다.
when(name){
// 게시글 목록 화면
ContentFragmentName.MAIN_FRAGMENT -> {
newFragment = MainFragment()
}
// 게시글 작성 화면
ContentFragmentName.ADD_CONTENT_FRAGMENT -> {
newFragment = AddContentFragment()
}
// 게시글 읽기 화면
ContentFragmentName.READ_CONTENT_FRAGMENT -> {
newFragment = ReadContentFragment()
}
// 게시글 수정 화면
ContentFragmentName.MODIFY_CONTENT_FRAGMENT -> {
newFragment = ModifyContentFragment()
}
}
// 메뉴
inflateMenu(R.menu.menu_read_content)
setOnMenuItemClickListener {
when (it.itemId) {
// 댓글
R.id.menuItemReadContentReply -> {
// 댓글을 보여줄 BottomSheet를 띄워준다.
showReplyBottomSheet()
}
// 수정
R.id.menuItemReadContentModify -> {
// 수정 화면이 보이게 한다.
contentActivity.replaceFragment(ContentFragmentName.MODIFY_CONTENT_FRAGMENT, true, true, null)
}
// 삭제
R.id.menuItemReadContentDelete -> {
}
}
true
}

글 읽기에서 수정 버튼을 누르면 ModifyContentFragment로 이동한다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
android:transitionGroup="true"
tools:context=".fragment.ModifyContentFragment">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbarModifyContent"
style="@style/Theme.AndroidProject4BoardApp.Toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="제목"
app:endIconMode="clear_text"
app:startIconDrawable="@drawable/subject_24px">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/textFieldModifyContentSubject"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/toggleModifyContentType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:selectionRequired="true"
app:singleSelection="true">
<Button
android:id="@+id/buttonModifyContentType1"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="자유"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<Button
android:id="@+id/buttonModifyContentType2"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="유머"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<Button
android:id="@+id/buttonModifyContentType3"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="시사"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<Button
android:id="@+id/buttonModifyContentType4"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="스포츠"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:hint="글 내용"
app:endIconMode="clear_text"
app:startIconDrawable="@drawable/description_24px">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/textFieldModifyContentText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text|textMultiLine"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/buttonModifyContentImageDelete"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="이미지 삭제"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<ImageView
android:id="@+id/imageViewModifyContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:adjustViewBounds="true"
app:srcCompat="@drawable/panorama_24px" />
</LinearLayout>
</ScrollView>
</LinearLayout>

// ModifyContentFragment.kt
class ModifyContentFragment : Fragment() {
lateinit var fragmentModifyContentBinding: FragmentModifyContentBinding
lateinit var contentActivity: ContentActivity
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
fragmentModifyContentBinding = FragmentModifyContentBinding.inflate(inflater)
contentActivity = activity as ContentActivity
settingToolbarModifyContent()
return fragmentModifyContentBinding.root
}
// 툴바 설정
fun settingToolbarModifyContent(){
fragmentModifyContentBinding.apply {
toolbarModifyContent.apply {
// 타이틀
title = "글 수정"
// Back
setNavigationIcon(R.drawable.arrow_back_24px)
setNavigationOnClickListener {
contentActivity.removeFragment(ContentFragmentName.MODIFY_CONTENT_FRAGMENT)
}
}
}
}
}

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menuItemModifyContentCamera"
android:icon="@drawable/photo_camera_24px"
android:title="카메라"
app:showAsAction="always" />
<item
android:id="@+id/menuItemModifyContentAlbum"
android:icon="@drawable/photo_album_24px"
android:title="앨범"
app:showAsAction="always" />
<item
android:id="@+id/menuItemModifyContentReset"
android:icon="@drawable/clear_all_24px"
android:title="초기화"
app:showAsAction="always" />
<item
android:id="@+id/menuItemModifyContentDone"
android:icon="@drawable/done_24px"
android:title="완료"
app:showAsAction="always" />
</menu>

// 툴바 설정
fun settingToolbarModifyContent(){
fragmentModifyContentBinding.apply {
toolbarModifyContent.apply {
// 타이틀
title = "글 수정"
// Back
setNavigationIcon(R.drawable.arrow_back_24px)
setNavigationOnClickListener {
contentActivity.removeFragment(ContentFragmentName.MODIFY_CONTENT_FRAGMENT)
}
// 메뉴
inflateMenu(R.menu.menu_modify_content)
}
}
}


// Tools.kt
// ContentActivity의 Fragments
enum class ContentFragmentName(var str:String){
MAIN_FRAGMENT("mainFragment"),
ADD_CONTENT_FRAGMENT("addContentFragment"),
READ_CONTENT_FRAGMENT("readContentFragment"),
MODIFY_CONTENT_FRAGMENT("modifyContentFragment"),
MODIFY_USER_FRAGMENT("modifyUserFragment"),
}
// ContentActivity.kt
// 이름으로 분기한다.
// Fragment의 객체를 생성하여 변수에 담아준다.
when(name){
// 게시글 목록 화면
ContentFragmentName.MAIN_FRAGMENT -> {
newFragment = MainFragment()
}
// 게시글 작성 화면
ContentFragmentName.ADD_CONTENT_FRAGMENT -> {
newFragment = AddContentFragment()
}
// 게시글 읽기 화면
ContentFragmentName.READ_CONTENT_FRAGMENT -> {
newFragment = ReadContentFragment()
}
// 게시글 수정 화면
ContentFragmentName.MODIFY_CONTENT_FRAGMENT -> {
newFragment = ModifyContentFragment()
}
// 사용자 정보 수정 화면
ContentFragmentName.MODIFY_USER_FRAGMENT -> {
newFragment = ModifyUserFragment()
}
}
// ContentActivity.kt
// 네비게이션 뷰 설정
fun settingNavigationView(){
activityContentBinding.apply {
navigationViewContent.apply {
// 헤더로 보여줄 view를 생성한다.
val headerContentDrawerBinding = HeaderContentDrawerBinding.inflate(layoutInflater)
// 헤더로 보여줄 View를 설정한다.
addHeaderView(headerContentDrawerBinding.root)
// 사용자 닉네임을 설정한다.
headerContentDrawerBinding.headerContentDrawerNickName.text = "홍길동님"
// 메뉴를 눌렀을 때 동작하는 리스너
setNavigationItemSelectedListener {
// 딜레이를 조금 준다.
SystemClock.sleep(200)
// 메뉴의 id로 분기한다.
when(it.itemId){
// 전체 게시판
R.id.menuItemContentNavigationAll -> {
// NavigationView를 닫아준다.
drawerLayoutContent.close()
}
// 자유 게시판
R.id.menuItemContentNavigation1 -> {
// NavigationView를 닫아준다.
drawerLayoutContent.close()
}
// 유머 게시판
R.id.menuItemContentNavigation2 -> {
// NavigationView를 닫아준다.
drawerLayoutContent.close()
}
// 시사 게시판
R.id.menuItemContentNavigation3 -> {
// NavigationView를 닫아준다.
drawerLayoutContent.close()
}
// 스포츠 게시판
R.id.menuItemContentNavigation4 -> {
// NavigationView를 닫아준다.
drawerLayoutContent.close()
}
// 사용자 정보 수정
R.id.menuItemContentNavigationModifyUserInfo -> {
replaceFragment(ContentFragmentName.MODIFY_USER_FRAGMENT, false, false, null)
// NavigationView를 닫아준다.
drawerLayoutContent.close()
}
// 로그아웃
R.id.menuItemContentNavigationLogout -> {
}
// 회원탈퇴
R.id.menuItemContentNavigationSignOut -> {
}
}
true
}
}
}
}


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
android:transitionGroup="true"
tools:context=".fragment.ModifyUserFragment">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbarModifyUser"
style="@style/Theme.AndroidProject4BoardApp.Toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="닉네임"
app:endIconMode="clear_text"
app:startIconDrawable="@drawable/person_add_24px">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/textFieldModifyUserInfoNickName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:hint="나이"
app:endIconMode="clear_text"
app:startIconDrawable="@drawable/face_24px">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/textFieldModifyUserInfoAge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:hint="비밀번호"
app:endIconMode="password_toggle"
app:startIconDrawable="@drawable/key_24px">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/textFieldModifyUserInfoPw1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text|textPassword"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:hint="비밀번호 확인"
app:endIconMode="password_toggle"
app:startIconDrawable="@drawable/key_24px">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/textFieldModifyUserInfoPw2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text|textPassword"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/toggleModifyUserInfoGender"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:selectionRequired="true"
app:singleSelection="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/buttonModifyUserInfoMale"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="남자"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<com.google.android.material.button.MaterialButton
android:id="@+id/buttonModifyUserInfoFemale"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="여자"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkBoxModifyUserInfoAll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="취미"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<LinearLayout
android:id="@+id/checkBoxGroupModifyUserInfo1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkBoxModifyUserInfoHobby1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="운동" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkBoxModifyUserInfoHobby2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="독서" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkBoxModifyUserInfoHobby3"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="영화감상" />
</LinearLayout>
<LinearLayout
android:id="@+id/checkBoxGroupModifyUserInfo2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkBoxModifyUserInfoHobby4"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="요리" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkBoxModifyUserInfoHobby5"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="음악" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkBoxModifyUserInfoHobby6"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="기타" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menuItemModifyUserDone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/done_24px"
android:title="완료"
app:showAsAction="always" />
</menu>

// ModifyUserFragment.kt
class ModifyUserFragment : Fragment() {
lateinit var fragmentModifyUserBinding: FragmentModifyUserBinding
lateinit var contentActivity: ContentActivity
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
fragmentModifyUserBinding = FragmentModifyUserBinding.inflate(inflater)
contentActivity = activity as ContentActivity
settingToolbarModifyUser()
return fragmentModifyUserBinding.root
}
fun settingToolbarModifyUser(){
fragmentModifyUserBinding.toolbarModifyUser.apply {
// 타이틀
title = "회원 정보 수정"
// 메뉴
inflateMenu(R.menu.menu_modify_user)
}
}
}

Model - 데이터 관리
View
ViewModel - 화면에 필요한 데이터 관리
안드로이드에서 공식적으로 지원을 한다.
Layout폴더의 xml파일에서 설정한 뷰의 id가 액티비티(프래그먼트)파일에서 생성된 바인딩 객체의 프로퍼티 이름이 되고 값으로 해당 뷰의 주소값이 들어간다.
액티비티(프래그먼트) 파일과 레이아웃 xml 파일은 종속적인 관계를 갖게된다.
레이아웃 xml에서 만든 뷰의 id가 바뀌면 액티비티(프래그먼트) 에서 사용하는 해당 뷰의 id도 바꿔줘야 한다.
액티비티(프래그먼트)에서 레이아웃xml에서 만든 뷰를 사용하지 않는다.
액티비티(프래그먼트)와 레이아웃 xml과의 종속적 관계를 없애고 독립성을 얻기 위함.
완벽하게 독립관계를 구현하기에는 매우 어려울지라도 가능한 종속성을 최소화 시킨다.
구글에서는 화면에 관련된 부분까지만 MVVM 구조로 작업하는 것을 권장하고 있다.
이벤트처리까지 모두 구현하려 하면 매우 힘들어짐.
게시글에 대한 정보 중 게시글번호, 제목, 내용이 있는 경우
화면에 보여질 정보는 제목, 내용만 필요할 때 ViewModel에서는 제목, 내용만을 관리한다.
사용자가 입력한 제목, 내용이 ViewModel에 담기고 액티비티(프래그먼트)는 ViewModel에 담긴 값을 가져와 사용한다.
액티비티(프래그먼트)는 ViewModel에 넣은 값이 어떤 뷰에 적용이 될 지 모른다.
레이아웃xml에서는 ViewModel에서 가져온 값을 어느 뷰의 속성에 넣을지를 정해주기만 하면 된다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
android:padding="10dp"
tools:context=".MainActivity" >
<EditText
android:id="@+id/edit1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="number" />
<EditText
android:id="@+id/edit2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="number" />
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button" />
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TextView"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</LinearLayout>

MVVM구조에서는 뷰바인딩 대신 데이터바인딩을 한다.
buildFeatures {
dataBinding = true
}

ViewModel 클래스를 상속받는다.
// TestViewModel.kt
class TestViewModel : ViewModel() {
// View에 설정할 값을 담을 객체
// 제네릭은 반드시 View의 속성에 대한 값의 타입으로 맞춰줘야 한다. (예를들어 text속성은 String타입)
val edit1Data = MutableLiveData<String>()
val edit2Data = MutableLiveData<String>()
val textViewData = MutableLiveData<String>()
}
기존의 레이아웃을 <layout> 태그로 감싸준다.
<?xml version="1.0" encoding="utf-8"?>
<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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp"
tools:context=".MainActivity" >
<EditText
android:id="@+id/edit1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="number" />
<EditText
android:id="@+id/edit2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="number" />
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button" />
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TextView"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</LinearLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<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>
<variable
name="testViewModel"
type="kr.co.lion.android53_mvvm.TestViewModel" />
</data>
<EditText
android:id="@+id/edit1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="number"
android:text="@={testViewModel.edit1Data}" />
<EditText
android:id="@+id/edit2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="number"
android:text="@={testViewModel.edit2Data}" />
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button" />
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={testViewModel.textViewData}"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
에디터에서 보면 해당 변수를 사용하고 있는 것으로 처리된다.

// MainActivity.kt
class MainActivity : AppCompatActivity() {
lateinit var activityMainBinding: ActivityMainBinding
lateinit var testViewModel: TestViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityMainBinding = DataBindingUtil.inflate(layoutInflater, R.layout.activity_main, null, false)
setContentView(activityMainBinding.root)
}
}
메인액티비티에서 생성한 viewmodel 객체를 레이아웃xml에서 사용할 수 있게 셋팅해줘야 한다.

// MainActivity.kt
class MainActivity : AppCompatActivity() {
lateinit var activityMainBinding: ActivityMainBinding
lateinit var testViewModel: TestViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityMainBinding = DataBindingUtil.inflate(layoutInflater, R.layout.activity_main, null, false)
testViewModel = TestViewModel()
activityMainBinding.testViewModel = testViewModel
// 데이터 바인딩 객체의 수명 설정
// 액티비티가 소멸되면 바인딩 객체도 소멸된다.
activityMainBinding.lifecycleOwner = this
testViewModel.textViewData.value = "안녕하세요"
setContentView(activityMainBinding.root)
}
}

ViewModel 클래스에 메서드를 작성한다.
// TestViewModel.kt
class TestViewModel : ViewModel() {
// View에 설정할 값을 담을 객체
// 제네릭은 반드시 View의 속성에 대한 값의 타입으로 맞춰줘야 한다. (예를들어 text속성은 String타입)
val edit1Data = MutableLiveData<String>()
val edit2Data = MutableLiveData<String>()
val textViewData = MutableLiveData<String>()
// 버튼을 누르면 동작하는 메서드
fun buttonClick(view: View){
// 입력한 값을 가져온다.
val data1 = edit1Data.value?.toInt()!!
val data2 = edit2Data.value?.toInt()!!
val r1 = data1 + data2
// 출력한다
textViewData.value = r1.toString()
}
}
레이아웃xml에 적용
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button"
android:onClickListener="@{ (view) -> testViewModel.buttonClick(view) }" />

// TestViewModel.kt
class TestViewModel : ViewModel() {
// View에 설정할 값을 담을 객체
// 제네릭은 반드시 View의 속성에 대한 값의 타입으로 맞춰줘야 한다. (예를들어 text속성은 String타입)
val edit1Data = MutableLiveData<String>()
val edit2Data = MutableLiveData<String>()
val textViewData = MutableLiveData<String>("test")
}
