📱 Android Studio Series - Fragment

임승현·2022년 4월 14일
0
post-thumbnail

ℹ️ Introduction

미루고, 미루고, 미루고 , 또 미루었던 안드로이드 스튜디오 관련 Post를 처음으로 작성하고자 합니다. C++ To Java Post도 그 끝을 알 수 없이 밀리고 있는 상황에서, 이렇게 앞뒤가 맞지 않을 수가 있냐라는 의문은 글쓴이의 머릿속에서도 떠나지 못했습니다.

죄송합니다. 나 자신에게. 이 글과 C++ To Java Post를 보시는 여러분들께.
6전공이 이렇게나 사람을 갉아먹을 줄 미처 알지 못했습니다.

Velog 시작하면서 작성했던 Intro 대로, 지금까지 작은 프로젝트들을 진행하면서 배운 개념들에 대해, 여러 Reference와 Source Code들을 재구성하여, 차근차근, 최대한 정확한 정보를 토대로 작성해 보도록 하겠습니다. 별다른 안내 문구가 없는 한, 이 시리즈의 Post가 포함한 모든 Source Code는 Java로 작성되었다고 생각하시면 됩니다.

이번 Post에서는 Fragment에 대해 다룹니다.

🎛 Why We Use Fragments?

Fragment는 FragmentActivity 내의 어떤 동작 또는 사용자 인터페이스의 일부를 나타냅니다.
여러 개의 프래그먼트를 하나의 액티비티에 결합하여 창이 여러 개인 UI를 빌드할 수 있으며, 하나의 프래그먼트를 여러 액티비티에서 재사용할 수 있습니다. 프래그먼트는 액티비티의 모듈식 섹션이라고 생각하면 됩니다. 이는 자체적인 수명 주기를 가지고, 자체 입력 이벤트를 수신하고, 액티비티 실행 중에 추가 및 삭제가 가능합니다.
(다른 액티비티에 재사용할 수 있는 "하위 액티비티"와 같은 개념이라고 생각하면 됩니다.)

안드로이드의 Activity는 애플리케이션의 화면을 구성하기 위한 기본 단위입니다. 하나의 Activity를 구성하다 보면 화면이 너무 복잡하거나 또는 코드의 양이 너무 많아졌거나 하는 이유로 화면의 부분별로 따로 동작시키고 싶을 때가 있습니다. 또한 하나의 Activity를 base로 두고, 그 위에서 여러 화면과 기능을 번갈아 표현하고 싶을 때도 있죠.

그럴 때 각각의 화면을 분할해서 독립적인 코드로 구성할 수 있게, 사용자가 2개 이상의 화면 사이에서 빠르게 이동할 수 있게 도와주는 것이 바로 Fragment 입니다.

Android가 Fragment를 도입한 것은 Android 3.0(API 레벨 11) 부터입니다. 기본적으로 Tablet과 같은 큰 화면에서 보다 역동적이고, 유연한 UI를 지원하는 것이 목적이었죠. Tablet은 Phone Device보다 화면이 훨씬 커서, 다양한 구성 요소들을 담을 공간도 그만큼 많습니다.

위의 그림을 잠깐 보겠습니다. Tablet에서는 List Fragment와 List Fragment 내에서의 특정 동작으로 인해 내부 요소가 변하는 Detail Fragment, 두 Fragment 모두 한 화면에 담을 수 있습니다. 그러나 (세로 화면 기준,) Phone Device에서는 List Fragment만 표시한 뒤, List Fragment 내에서 사용자가 특정 동작을 수행해야, Detail Fragment가 나타나는 형태로만 (Tablet에서의 UI와 같은 기능을) 구현할 수 있죠. (일반 스마트폰에서의 카카오톡 화면과 태블릿 혹은 Galaxy Fold에서의 카카오톡 화면을 비교해 보시면 이해가 빠를 듯 합니다.)

이렇게 Fragment를 사용하면, 하나의 Activity로 조건(실행되는 Device의 특성)에 따라 서로 다른 화면 구성을 만들 수 있습니다.

1️⃣ 위 그림에서 왼쪽의 형태는, 한 번에 1개의 Fragment가 화면에 나타나는 형태로 Fragment 여러 개를 미리 만들어두고 탭 메뉴나 스와이프로 화면 간 이동을 할 때 사용됩니다. (뒤에서 BottomNavigationView를 이용해 Fragment 전환을 구현하는 방법을 다룹니다.)

2️⃣ 오른쪽의 형태는, 한 번에 여러 개의 Fragment가 동시에 화면에 나타나는 형태로 Tablet과 같은 대형 화면을 가진 Device에서 메뉴와 뷰를 한꺼번에 나타내거나 여러 개의 섹션을 모듈화한 뒤 한 화면에 나타낼 때 사용됩니다.

이 글에서는 주로 1️⃣을 어떻게 구현해야 할지에 대한 내용을 다룹니다.

🔨 Create a Fragment

본격적으로 Fragment에 대해 다루기 전에, 이 글에서 다룰 예시(간단한 애플리케이션)에 대해 잠깐 살펴보겠습니다.
MainActivity.java 위에 Fragment_SelectPlantFragment_Watering의 화면이 번갈아 나타납니다. 화면 간의 이동은 BottomNavigationView를 통해 이루어지며, 데이터의 이동은 Fragment_SelectPlant로부터 Fragment_Watering으로의 단방향 전달만 이루어집니다.
이 시리즈에서는 위 프로젝트의 소스코드 전체를 오픈하지 않으며, 필요한 부분에 대해서만 일부 발췌해 관련 개념을 설명하는 데 사용될 예정입니다. (아마 잘 조합하면 전체 그림이 그려질지도..?)
또한 기본적으로, 위를 구현하는 데 필요한 기술만 다루되, 추가되는 내용이 있을 수 있습니다. 참고해 주세요.

Java 디렉토리 밑에 있는 패키지 명 (여기서는 com.example.watertheflowers)을 선택하여 마우스 우클릭하면 나타나는 메뉴에서 위 그림과 같이 선택해 Fragment(Blank)를 생성합니다. 그리고 New Android Component 창에서 Fragment 이름을 지정하고, 생성된 .java 파일의 public class를 아래 소스코드와 같이 수정합니다.

📁 Fragment_SelectPlant.java

public static class Fragment_SelectPlant extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment__select_plant, container, false);
        return rootView;
    }
}

📁 Fragment_Watering.java

public static class Fragment_Watering extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment__watering, container, false);
        return rootView;
    }
}

MainActivity에 Fragment를 추가하는 방법은 BottomNavigationView와 함께 "↔️ Fragment Transaction" 파트에서 다룹니다. 그 전에 FragmentManager에 대해서 알아보겠습니다.

⚠️ 참고: OnCreateView의 Parameter

inflater는 레이아웃 파일을 로드하기 위한 레이아웃 인플레이터를 제공합니다. container는 Fragment 레이아웃이 배치되는 부모 레이아웃, 여기서는 즉 MainActivity의 레이아웃을 의미하고, savedInstanceState는 일종의 Bundle 로서, 상태값 저장을 위한 보조 도구입니다.
Bundle 은 Fragement가 복원, 즉 재개 중인 경우, Fragment의 이전 Instance에 대한 데이터를 제공합니다.

👮 Fragment Manager

📁 Fragment_SelectPlant.java 파일의 public class의 일부분.

public static class Fragment_SelectPlant extends Fragment {}

여러분은 Fragment Class를 상속받은 Fragment_SelectPlant Class를 만들었습니다. 하지만, 아직 사용자가 Fragment 화면을 볼 수 있는 것은 아닙니다. 여러분의 MainActivity에는 아무것도 구현되어 있지 않기 때문이죠. Fragment를 본격적으로 활용하려면, MainActivity 내에 FragmentManager 객체를 생성해야 합니다. 객체의 생성에는getSupportFragmentManager() 라는 메소드가 필요합니다.

Fragment fa = new Fragment_SelectPlant();

A. 같은 패키지 내의 클래스 자체를 가져와 인스턴스를 생성하는 방법도 있습니다.
(위의 메서드는 Activity에서 Fragment 안의 메서드를 호출할 때 사용합니다.)

B. addToBackStack() 메소드를 사용하면 Fragment를 삽입하기 위해 사용되는 Transaction을 마치 하나의 Activity처럼 백스택에 담아둘 수 있습니다. 따라서 스마트폰의 뒤로가기 버튼으로 Transaction 전체를 마치 Activity처럼 제거할 수 있게 됩니다.

  • 고돈호, 『이것이 안드로이드다 with 코틀린』, 한빛미디어(2020), p336.

C. 바로 다음 파트에서 다룹니다!

↔️ Fragment Transaction

❓Transaction이란 무엇일까요❓

FragmentTransaction 객체 생성

FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

위 소스코드는 Android Developers 사이트에서 제공되는 코드입니다. 여기서는 FragmentManager Class의 beginTransaction() 메소드를 이용해 FragmentTransaction 객체를 생성합니다. 아래 BottomNavigationView와 다룰 예제 코드에서는 위와 같이 객체를 따로 생성하지 않고, FragmentTransaction Class의 메소드를 사용해 두 Fragment 간 화면 전환을 다룹니다.

📁 MainActivity.java의 일부
↔️ BottomNavigationView를 활용한 화면 전환 구현

  • 필요한 인스턴스 생성
BottomNavigationView bottomNavigationView = findViewById(R.id.bottomNavi); // bottomNavigationView 인스턴스 생성
FragmentManager fragmentManager = getSupportFragmentManager(); // FragmentManager 인스턴스 생성
  • 시작 화면
fa = new Fragment_SelectPlant(); // Fragment 객체 생성
fragmentManager.beginTransaction().add(R.id.main_frame,fa).commit(); // 앱 초기 화면을 fragment__select_plant.xml로 설정.
  • BottomNavigationView를 이용한 Fragment 전환 구현
 bottomNavigationView.setOnItemSelectedListener((BottomNavigationView.OnItemSelectedListener) menuItem -> {
            if(menuItem.getItemId() == R.id.select_plants ) { //menuItem엔 각각의 Fragment를 지칭하는 Id를 가진 요소가 있음.
                if (fa == null) {
                    fa = new Fragment_SelectPlant();
                    fragmentManager.beginTransaction().add(R.id.main_frame, fa).commit();
                }
                if (fa != null)
                    fragmentManager.beginTransaction().show(fa).commit();
                if (fb != null)
                    fragmentManager.beginTransaction().hide(fb).commit();
            }
            else if(menuItem.getItemId() == R.id.watering){
                if(fb == null) {
                    fb = new Fragment_Watering();
                    fragmentManager.beginTransaction().add(R.id.main_frame, fb).commit();
                }
                if(fb != null)
                    fragmentManager.beginTransaction().show(fb).commit();
                if(fa != null)
                    fragmentManager.beginTransaction().hide(fa).commit();
            }
            return true;
        });

위 소스코드에 대해서 간단하게 설명드리자면, 먼저 앱 초기 화면을 나타낼 Fragment를 Fragment_SelectPlant로 설정했습니다. 그리고 bottomNavigationViewsetOnItemSelectedListener 안에서, 어떤 menuItem이 사용자에 의해 선택되었는지, Fragment들이 이미 생성되어 있는지, 그렇지 않은지에 따라 케이스를 분리해 작성했습니다.

위 소스코드를 좀 더 명확하게 이해하기 위해 알아야 할, FragmentTransaction Class의 주요 메소드들과 각각의 역할에 대해 아래에 표로 정리해 놓았습니다.

⚠️ Fragment Transaction 메소드 사용 시 주의사항
1. commit()을 마지막으로 호출해야 합니다.
2. 같은 Container에 여러 개의 Fragment를 추가하는 경우, 이를 추가(Commit) 하는 순서에 따라 이들이 View에 나타나는 순서가 결정됩니다.

위 코드를 FragmentTransaction Class 메소드들의 역할과 함께 설명한다면 다음과 같습니다.

Application 실행 시

Fragment_SelectPlant Class 의 인스턴스를 생성.
add()를 이용해 생성된 Fragment 인스턴스를 레이아웃에 추가하고,
변경내용을 commit()함으로써 MainActivity 에 반영.

1. BottomNavigationView에서 SelectPlant 모드를 선택한 경우

if (Fragment_SelectPlant Class 의 인스턴스가 생성되지 않음.)
------- 인스턴스를 생성하고, add() 그리고 commit() 하기.
if (Fragment_SelectPlant Class 의 인스턴스가 이미 생성된 경우.)
------- hide되었던 Fragment 인스턴스를 show()를 통해
------- MainActivity에 올리고, 변경사항을 commit()함.
if (Fragment_Watering Class 의 인스턴스가 이미 생성된 경우.)
------- MainActivity에 올라와 있는 Fragment 인스턴스를 hide()함.

2. BottomNavigationView에서 Watering 모드를 선택한 경우

if (Fragment_Watering Class 의 인스턴스가 생성되지 않음.)
------- 인스턴스를 생성하고, add() 그리고 commit() 하기.
if (Fragment_Watering Class 의 인스턴스가 이미 생성된 경우.)
------- hide되었던 Fragment 인스턴스를 show()를 통해
------- MainActivity에 올리고, 변경사항을 commit()함.
if (Fragment_Watering Class 의 인스턴스가 이미 생성된 경우.)
------- MainActivity에 올라와 있는 Fragment 인스턴스를 hide()함.

지금까지 MainActivity 위에 올라가는 Fragment들 간의 전환 방법에 대해 알아보았습니다. 이제 Fragment 사이에서 데이터 전송이 어떻게 이루어지는지 살펴보겠습니다.

💬 Communicating Between Fragments

Fragment 사이의 통신은 공유 ViewModel 또는 Fragment Result API를 통해 이루어집니다. 이 포스트에서는 하나의 Fragment에서 다른 하나의 Fragment로 일회성 데이터를 전송하는 예시에 적합한 Fragment Result API 만을 다룹니다.

Fragment 사이의 통신을 가능하게 하는 두 가지 옵션에 대한 자세한 설명은 아래 표로 정리해 두었습니다. (ViewModel에 대해서는 추후에 다시 다루겠습니다.)

위 표에서 다룬 바와 같이, FragmentManager Class는 FragmentResultOwner를 통해 Fragment 결과의 저장소 역할을 합니다. 여러 개의 Fragment가 계속해서 상호 참조할 필요 없이, 하나의 Fragment가 공유할 데이터를 setFragmentResult()를 통해 FragmentManager에 올려만 놓으면, 다른 Fragment들은 그에 대응하는 requestKeysetFragmentResultListener()를 통해 그 데이터에 손쉽게 접근할 수 있습니다. 이 과정을 도식화하면 아래 그림과 같습니다.

아래는 위에서 다룬 메서드들을 이용해 Fragment_SelectPlant에서 Fragment_Watering으로 Bundle 형태의 데이터가 전송되는 방식을 다룬 소스코드입니다.

1️⃣ : setFragmentResult() 사용 예제
📁 Fragment_SelectPlant.java 중 일부

save_plant.setOnClickListener(v -> { //특정 버튼을 눌렀을 때
            //(중략)
            if(임의의 조건 충족시) {
                //(중략)
                Bundle bundle = new Bundle();
                //Bundle에 Key와 그에 대응되는 String-Type Value 저장.
                bundle.putString("recommended_humidity", String.valueOf(plantsDataList[position].getRecommended_humidity()));
                bundle.putString("recommended_amount", String.valueOf(plantsDataList[position].getRecommended_amount())); 
                //아래에서 자세히 설명.
                getParentFragmentManager().setFragmentResult("key", bundle);
            }
            else
                //(예외 처리)
 });

2️⃣&3️⃣ : setFragmentResultListener() 사용 예제
📁 Fragment_Watering.java 중 일부

super.onCreate(savedInstanceState);
//리스너에 대해서는 아래에서 자세히 설명.
getParentFragmentManager().setFragmentResultListener("key", this, (requestKey, result) -> {
        	//Listener로 받아온 Bundle에 포함된 데이터를 미리 지정한 Key를 통해 가져옴.
            String amount_value = result.getString("recommended_amount");
            String humidity_value = result.getString("recommended_humidity");
            //(중략, 프로그램 기능에 맞게 처리)
});

setFragmentResult()setFragmentResultListener()에 대해 더 자세히, 더 쪼개서 살펴보면,

📡 setFragmentResult()

📲 setFragmentResultListener()

⚠️ 참고
getParentFragmentManager()를 호출하여 두 메서드 setFragmentResult()setFragmentResultListener()를 사용한 이유는 두 Fragment가 동등한 관계에, 또는 결과를 수신하는 Fragment가 결과를 setting한 Fragment보다 하위에 있기 때문입니다.
상위 Fragment가 하위 Fragment가 생성한 결과를 Listener로 받아오려면, getChildFragmentManager()를 호출해야 합니다.

🔜🔁 Fragment Lifecycle

📄 References

🧭 안드로이드 개발자 - developer.android.com
📖 고돈호, 『이것이 안드로이드다 with 코틀린』, 한빛미디어(2020), p321-347.

profile
안드로이드/아이폰 앱 개발에 막 관심이 생긴 컴퓨터공학과 (마음만은) 새내기. 현대미술과 음악, 철학적 혹은 정치적 사유, 가끔 독서도 즐김.

0개의 댓글

관련 채용 정보