* 시작에 앞서
개인 프로젝트, 코딩 과정을 상세하게 기록하고 리뷰하기 위해 작성된 블로그입니다.
공부와 함께 진행되는 스파르타식 프로젝트이므로, 부족한 부분이 많을 수 있습니다.
Feedback은 언제나 환영입니다.
본 포스트는 Android Studio 환경에서 Kotlin 으로 작성된 Fragment 구성에 대한 포스트입니다.
Android Developer Guide의 권장사항은 다수의 화면일 때, New Activity가 아닌 Fragment 사용을 권장한다.
* Fragment ?
Fragment는 Activity 내에서 UI의 일부분을 나타내는 요소로, 한 화면에 여러개의 화면을 보여주기 위해 많이 사용된다.
물론 여러개의 Activity를 생성, 사용해서 화면을 보여 줄 수 있지만, Android Guide에서는 일반적인 카K오톡, F이스북, 인★그램 등과 같은 하단 탐색 바가 있는 전역적인 요소를 사용하면서 네비게이션 선택에 따라 컨텐츠가 보이는 부분은 프래그먼트로 사용하는 것을 권장 하고 있다.
※ 화면이 하나만 필요한 경우는 프래그먼트를 사용하지 않고, 다수의 화면이 필요할 때 사용하길 권장
* Fragment 요소
Fragment
- Activity 내에서 UI의 일부분을 나타내는 요소, Activity의 부분 집합이 될 수 있음.
Fragment Manager
- Activity의 FarmeLayout에 Fragment를 등록, 교체, 삭제 등의 기능을 포함한 클래스
- Fragment 추가, 교체, 삭제 등의 변경 사항은 FragmentTransaction이라는 객체에서 등록 후 Commit()으로 실행
- FragmentTransaction 객체는 supportFragmentManager.beginTransaction() 함수의 반환 값.
FragmentLayout
- Activity 내에서 화면 전환이 필요한 구역의 컨테이너로 사용
supportFragmentManager.beginTransaction()
- Fragment를 Manage 할 수 있는(Fragment를 교체할 수 있고, 관리할 수 있는)
- beginTransaction()은 트랜잭션을 begin (시작) 하는
- => Fragment Transaction을 begin 하되, Manage 할 수 있는
* Fragment 구현하기
- FrameLayout으로 구현하되, SWITCH와 REMOVE 버튼 구현, FragmentA와 B 구현
- SWITCH 버튼을 누르면 FrameLayout에 FragmentA와 FragmentB가 교차되는 기능 구현
- REMOVE 버튼을 누르면 FrameLayout의 Fragment 연결이 제거되는 기능 구현
* 프로젝트 생성
- 프로젝트 명 : Fragment
- 언어 및 환경: Kotlin (Android Studio)
* Fragment 구현하기 (ViewBinding 환경 설정)
모듈 단의 build.gradle에 아래 코드를 추가한다.
android {
...
viewBinding {
enabled=true
}
...
...
}
코드를 추가하고, 아래와 같이 Sync Now를 클릭하여 Gradle 변경 사항을 적용한다. (필수)
ViewBinding은 Layout에 있는 View의 ID를 코틀린 코드에서 직접 사용할 수 있도록 해 주는 도구로, 기존의 Kotlin-android-extensions로 지원되었던 것을 대체한 기능이다.
※ kotlin-android-extensions와 ViewBinding의 차이
* kotlin-android-extensions : View의 ID로 코드에서 직접적인 접근 (기존의 방법)
- 코드에 사용 될 모든 View의 ID가 고유한 값을 가져야 함.
- 따라서 코드가 지저분해 질 수 있음.
- 중복된 ID를 사용하는 View 참조시 에러가 발생할 수 있음.
* ViewBinding : XML 레이아웃 파일의 결합 클래스를 사용 (현재의 방법)
- 유효하지 않은 View ID로 인한 Null 포인터 예외가 발생할 위험이 없음.
- XML 레이아웃 전체를 클래스화 하기 때문에, 레이아웃과 코드의 비호환성 문제가 없음.
*Fragment 추가하기
위와 같은 과정으로 Fragment 2개 (Fragment_A / Fragment_B) 를 생성한다.
그러면 이와 같이 2개의 .kt 파일과 .xml 레이아웃 파일이 생성된다.
UI 만들기
각 레이아웃.xml에 아래와 같이 작성한다.
* activity_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=".MainActivity">
<FrameLayout
android:id="@+id/frameLayout"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="12dp"
app:layout_constraintBottom_toTopOf="@+id/btnSwitch"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
<Button
android:id="@+id/btnSwitch"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:text="Switch"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btnRemove"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
/>
<Button
android:id="@+id/btnRemove"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:text="Remove"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/btnSwitch"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
* fragment_a.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".Fragment_A">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/teal_200"
android:text="This is Fragment_A"
android:textColor="#000000"
android:textSize="36sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
* fragment_b.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".Fragment_A">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/purple_200"
android:text="This is Fragment_B"
android:textColor="#000000"
android:textSize="36sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
여기까지 진행 된 UI 화면은 아래와 같다. (왼쪽부터 activity_main.xml / fragment_a.xml / fragment_b.xml 순)
💠 코틀린 코드 작성1 : Fragment 사용
MainActivity.kt의 FrameLayout에 FragmentA를 띄우기 위한 코드는 아래처럼 작성한다.
package com.jjangdeuk.fragment
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.jjangdeuk.fragment.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
lateinit var binding : ActivityMainBinding
// 전역변수로 바인딩 객체 선언
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 자동 생성된 View Binding Class에서의 inflate라는 Method를 활용해서
// Activity에서 사용할 Binding Class의 인스턴스 생성
binding = ActivityMainBinding.inflate(layoutInflater)
// getRoot method로 레이아웃 내부의 최상위 위치 뷰의
// 인스턴스를 활용하여 생성된 View를 액티비티에 표시함.
val view = binding.root
setContentView(view)
// setContentView(binding.root) 로 사용 가능
binding.btnSwitch.setOnClickListener {
setFragment()
}
}
//supportFragmentManager 는 Fragment를 관리하는 녀석을 불러온 것
// .beginTransaction()은 Transaction (작업)을 begin (시작) 하는 것.
private fun setFragment() {
val transaction = supportFragmentManager.beginTransaction()
.add(R.id.frameLayout, Fragment_A())
// frameLayout 부분에 Fragment_A 이름을 가진 view를 add하여 띄워주라는 액션
transaction.commit()
// 지금 실행한 transaction을 commit (저장) 하는 액션
}
}
* lateint
- 멤버변수는 ?를 사용해 선언하고 Null Check를 해 줘야 하는데, 해당 예약어를 사용하면 해당 변수는 ?로 선언되어 nullable 하다는 표시를 하지 않아도 된다.
- 전역변수로 선언 후 null 값을 지정하지 않고 초기화 하는 방법으로, var 키워드를 사용해 선언해야 한다.
- 접근/사용 전 변수가 초기화된다는 확신을 가지고 초기 선언/생성 시 초기화하지 않을 경우 사용한다.
- 말 그대로 늦은(late) 선언(initialize)으로 해석할 수 있다.
* 참고한 글
https://blog.mindorks.com/learn-kotlin-lateinit-vs-lazy
* ActivityMainBinding 및 viewBinding, binding 등
https://timradder.tistory.com/20 를 참고하자. 도움이 많이 될 것이다.
< 현재 구현된 화면 >
💠 코틀린 코드 작성2 : 최종 코드
Fragment 처리를 위해 생성된 FragmentTransaction 객체에는 위 코드에서 사용한 add() method 이외에 replace(), remove() 등의 method를 사용할 수 있다. 이 method들을 사용하여 MainActivity.kt의 기능을 덧 붙여보자.
package com.jjangdeuk.fragment
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.jjangdeuk.fragment.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
lateinit var binding : ActivityMainBinding
// 전역변수로 바인딩 객체 선언
var flag = 0
/*
- when flag is 0 : 초기 화면
- when flag is 1 : Fragment_A 화면으로 replace
- when flag is 2 : Fragment_B 화면으로 replace
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 자동 생성된 View Binding Class에서의 inflate라는 Method를 활용해서
// Activity에서 사용할 Binding Class의 인스턴스 생성
binding = ActivityMainBinding.inflate(layoutInflater)
// getRoot method로 레이아웃 내부의 최상위 위치 뷰의
// 인스턴스를 활용하여 생성된 View를 액티비티에 표시함.
val view = binding.root
setContentView(view)
// setContentView(binding.root) 로 사용 가능
binding.btnSwitch.setOnClickListener {
switchFragment()
// binding 변수를 활용하여 xml 파일 내의 View id에 접근하였음.
// btnSwitch = SWITCH 버튼.
// View id도 pascalCase + camelCase의 Naming 규칙 적용으로 인해
// btn_swtich 이거나 하면 btnSwtich 로 자동 변환되게 됨.
}
binding.btnRemove.setOnClickListener {
removeFragment()
}
}
// SWITCH 버튼에 대한 Fragment
// 버튼을 누르면 FrameLayout 부분에 표시되는 Fragment가 A <--> B 간에 서로 전환됨
private fun switchFragment() {
val transaction = supportFragmentManager.beginTransaction()
when(flag) { //supportFragmentManager 는 Fragment를 관리하는 녀석을 불러온 것
0 -> { // .beginTransaction()은 Transaction (작업)을 begin (시작) 하는 것.
transaction.add(R.id.frameLayout, Fragment_A())
flag=1
}
1 -> {
transaction.replace(R.id.frameLayout, Fragment_B())
flag=2
}
2 -> {
transaction.replace(R.id.frameLayout, Fragment_A())
flag=1
}
}
transaction.addToBackStack(null)
transaction.commit()
}
// REMOVE 버튼에 대한 Fragment
// 버튼을 누르면 FrameLayout 부분에 연결된 Fragment가 삭제됨.
private fun removeFragment() {
val transaction = supportFragmentManager.beginTransaction()
val frameLayout=supportFragmentManager.findFragmentById(R.id.frameLayout)
transaction.remove(frameLayout!!)
transaction.commit()
}
}
백스택 설정
transaction.addToBackStack(null)
Fragment는 기본적으로 backStack에 저장되지 않는다. backStack에 저장하고 싶은 경우 FragmentTransaction에 위와 같은 코드를 추가하여 뒤로가기 버튼 클릭 시 이전 화면을 볼 수 있도록 설정이 가능하다.
❓ backStack이 뭐야..?
여차저차 하여 위 코드대로 작성하면 결과는 아래와 같다.
- Switch 버튼 : FrameLayout 단에 표시되는 Fragment A와 B가 서로 전환됨
- Remove 버튼 : FrameLayout 단에 표시되는 Fragment가 삭제됨.
< 최종 구현된 화면 >