[Android] Dialog Fragment & Fragment간 통신

Oxong·2021년 6월 24일
0

21.06.23

공부한 것을 정리하는 용도의 글이므로 100% 정확하지 않을 수 있습니다.
참고용으로만 봐주시고, 내용이 부족하다고 느끼신다면 다른 글도 보시는 것이 좋습니다.
+ 틀린 부분, 수정해야 할 부분은 언제든지 피드백 주세요. 😊

                                                 by. ryalya



들어가기 전

[Android] Fragment란? 에서 Fragment 생명주기와 종류에 대해 알아보았다.

Android Dialog를 사용하기 위해 안드로이드 개발자 사이트를 참고할 때,
Dialog, AlertDialog, DatePickerDialog(TimePickerDialog)를 설명하면서
아래 이미지 처럼 Fragment를 사용하라고 권고하는 것을 볼 수 있었다.

그래서 Fragment에 관심을 가지게 되었던 건데...
그렇게 효율적이고 좋다면 사용하는 것이 좋으니까 Dialog Fragment를 사용하여 개발해보기로 했다.

(Fragment를 사용할거라면 개발 초기에 작성하는 것이 추후 코드를 변환하는 것보다 낫다고 한다.)

※ 위의 내용은 안드로이드 개발자 사이트에서 Dialog(대화상자)를 보면 자세하게 나와있다.



Fragment간 통신

인터넷에 Fragment간 통신에 대해 검색해보면 아래와 같은 방법들이 있다.

  • Bundle
  • ViewModel
  • Fragment Result API
  • Interface 이용한 리스너 구현

🟣 Bundle 사용

Fragment로부터 상속받는 setArguments(), getArguments()함수를 사용하여 Bundle을 주고 받을 수 있다.

Bundle의 객체는 map처럼 key,value 쌍으로 여러 데이터를 담을 수 있다.

전달하고자하는 Fragment Class

// * Java *
 Bundle bundle = new Bundle();
 bundle.putString("key", "value");

DialogFragment dialogFragment = new DialogFragment ();
dialogFragment.setArguments(args);
dialogFragment.show(getFragmentManager(), "Sample Dialog Fragment");


// * Kotlin *
// PassBundleFragment는 전달하고자 하는 Class를 말함.
 PassBundleFragment().arguments = bundle
 parentFragmentManager.beginTransaction()
	.replace(R.id.fragment_container_bundle, PassBundleFragment())
	.commit()

받을 Fragment Class

// * Java *
Bundle mArgs = getArguments();
String mValue = mArgs.getString("key");

// * Kotlin * 
override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
	var result = arguments?.getString("key")
	
    	return inflater.inflate(R.layout.fragment_pass_b, container, false)
    }


🟣 ViewModel 이용

하나의 Activity에서 container로 여러 Fragment를 이동하는 경우 사용.

ViewModel은 UI와 관련된 data를 생명주기를 신경쓰면서 저장하거나 관리하기위해서 설계된 클래스.

ViewModel은 Activity의 lifecycle보다 오래 살아있다(creen rotation 과 같은 configuration변화에서도 살아남는다.)

따라서 공통의 Activity의 ViewModel을 사용하여 데이터 전달이 가능하다.

나는 이 블로그를 참고하여 ViewModel 통신을 이해했다.

(app)Build.gradle에 의존성 추가

//LiveData and ViewModel
def lifecycle_version = "1.1.1"
implementation "android.arch.lifecycle:extensions:$lifecycle_version"

아래 코드들은 모두 블로그의 작성자분이 사용한 예시 코드이다.

작성자분은 객체로 LiveData를 선택했지만 데이터 타입 선택은 자유.

FragmentA 에서는 FragmentB가 입력한 데이터를 받아서 TextView를 갱신.

Activity를 오너로 등록하기 위해 Activity생성 지점인 onActivityCreated에서 sharedViewModel 인스턴스를 얻고,
뷰모델을 통해 LiveData타입인 텍스트를 얻어 observe.

public class SharedViewModel extends ViewModel {

    private MutableLiveData<String> liveText = new MutableLiveData<>();

    public LiveData<String> getText(){
        return liveText;
    }

    public void setText(String text){
        liveText.setValue(text);
    }

}
public class FragmentB extends Fragment {

    private SharedViewModel sharedViewModel;
    private EditText editText;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_b, container, false);
        editText = view.findViewById(R.id.input);
        return view;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        editText.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {

            }

            @Override
            public void afterTextChanged(Editable s) {
                if(sharedViewModel!=null){
                    sharedViewModel.setText(s.toString());
                }
            }
        });
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        sharedViewModel = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
    }
}
public class FragmentA extends Fragment {
    private SharedViewModel sharedViewModel;
    private TextView textView;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_a, container, false);
        textView = view.findViewById(R.id.input);
        return view;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        sharedViewModel = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        sharedViewModel.getText().observe(this, new Observer<String>() {
            @Override
            public void onChanged(@Nullable String s) {
                textView.setText(s);
            }
        });
    }
}


🟣 Result API 사용

FragmentManager는 fragment 결과의 중앙 저장소 역할을 하여 구성요소가 서로를 직접 참조하지 않아도 fragment 결과를 설정하고 이러한 결과를 수신 대기하여 구성요소가 서로 통신하는 방법.

이 방법은 안드로이드 개발자 사이트를 참고했다.

먼저 의존성 추가

//(AppModule)
dependencies {
	...
    implementation "androidx.fragment:fragment-ktx:1.3.0"
}

fragment B에서 fragment A로 데이터를 다시 전달하려면 우선, 결과를 수신하는 fragment인 fragment A에서 결과 리스너를 설정한다.

아래와 같이 fragment A의 FragmentManager에서 setFragmentResultListener()를 호출.

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getParentFragmentManager().setFragmentResultListener("requestKey", this, new FragmentResultListener() {
        @Override
        public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
            // We use a String here, but any type that can be put in a Bundle is supported
            String result = bundle.getString("bundleKey");
            // Do something with the result
        }
    });
}
위의 이미지는 FragmentManager를 사용하여 Fragment A로 데이터를 전송하는 Fragment B.

결과를 생성하는 Fragment인 Fragment B에서 동일한 requestKey를 사용하여 동일한 FragmentManager에 결과를 설정해야 한다.

이 작업은 setFragmentResult() API를 사용하여 실행할 수 있다.

  button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Bundle result = new Bundle();
        result.putString("bundleKey", "result");
        getParentFragmentManager().setFragmentResult("requestKey", result);
    }
});

그러면 Fragment A가 결과를 수신하고 STARTED 상태가 되면 리스너 콜백을 실행한다.

( ※ 주의 :

  • 백 스택의 Fragment는 표시되어 STARTED 상태가 될 때까지 결과를 수신하지 않는다.
  • 결과를 수신 대기하는 Fragment가 STARTED 상태인 경우 결과가 설정되면 리스너의 콜백이 즉시 실행된다.)

FragmentScenario를 사용하여 setFragmentResult() 및 setFragmentResultListener() 호출을 테스트.

launchFragmentInContainer 또는 launchFragment를 사용하여 테스트 중인 Fragment는의 시나리오를 만들고 테스트 중이 아닌 메서드를 수동으로 호출.

setFragmentResultListener()를 테스트하려면 setFragmentResultListener()를 호출하는 Fragment는로 시나리오를 만든 후, setFragmentResult()를 직접 호출하여 결과 확인.

@Test
fun testFragmentResultListener() {
    val scenario = launchFragmentInContainer<ResultListenerFragment>()
    scenario.onFragment { fragment ->
        val expectedResult = "result"
        fragment.parentFragmentManager.setFragmentResult("requestKey", bundleOf("bundleKey" to expectedResult))
        assertThat(fragment.result).isEqualTo(expectedResult)
    }
}

class ResultListenerFragment : Fragment() {
    var result : String? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Use the Kotlin extension in the fragment-ktx artifact
        setFragmentResultListener("requestKey") { requestKey, bundle ->
            result = bundle.getString("bundleKey")
        }
    }
}

하위 fragment의 결과를 상위 프래그먼트로 전달

(상위 fragment는 setFragmentResultListener()를 호출할 때 getParentFragmentManager() 대신 getChildFragmentManager()를 사용)

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // We set the listener on the child fragmentManager
    getChildFragmentManager()
        .setFragmentResultListener("requestKey", this, new FragmentResultListener() {
            @Override
            public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
                String result = bundle.getString("bundleKey");
                // Do something with the result
            }
        });
}

하위 Fragment는 FragmentManager에 결과를 설정합니다. 그러면 다음과 같이 Fragment가 STARTED 상태가 되면 상위 Fragment에서 결과를 수신.

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Bundle result = new Bundle();
        result.putString("bundleKey", "result");
        // The child fragment needs to still set the result on its parent fragment manager
        getParentFragmentManager().setFragmentResult("requestKey", result);
    }
});


Reference

Fragment 통신
: https://developer.android.com/guide/fragments/communicate
: https://www.charlezz.com/?p=1062

0개의 댓글