본문 바로가기

Study/Kotlin Study

[Kotlin] Fragment 실습

+ 항공대학교 김철기 교수님의 객체 지향 프로그래밍 과목 내용를 정리한 글입니다.

build.gradle.kts 수정하기

android 블록 안에 코드를 추가한다.

- XML 레이아웃 파일과 Kotlin간의 상호작용을 향상시켜주는 라이브러리를 활성화한다.
- 자동으로 binding 클래스가 생성되어 XML 레이아웃과 상호작용이 가능하다.

ex) MainActicity -> ActivityMainBinding 인스턴스 생성

ex) InputFragment -> FragmentInputBinding 인스턴스 생성

viewBinding{
        enable = true
}

 

 

dependencies 블록에 코드를 추가한다.

- dependency(종속성): 프로젝트에서 사용하려는 외부 코드를 프로젝트로 가져오는 방법 (다른 개발자가 만든 라이브러리 사용)

- AndroidX 라이브러리의 일부인 fragment-ktx 라이브러리를 프로젝트에 추가한다.
- implementaion: 종속성을 추가할 때 사용되는 함수

dependencies {
    implementation("androidx.fragment:fragment-ktx:1.6.1") // 추가
    implementation("androidx.core:core-ktx:1.9.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.9.0") // 버전 수정 (1.10.0 -> 1.9.0)
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}

 

=> gradle 파일을 수정하고 좌측 상단의 Sync now 버튼을 눌러 적용시킨다.


레이아웃 설정하기

+ 레이아웃을 설정할 때는 Palette에 있는 요소를 Component Tree에 드래그하여 설정한다.

 

기본적으로 가장 바깥쪽 레이아웃은 ConstraintLayout으로 설정되어있다. (변경 불가)

 

+ LinearLayout: 구성요소를 나열하기 위한 레이아웃

+ FrameLayout: 구성요소를 꽉 채우기 위한 레이아웃 (하나의 구성요소만 가능)

+ ConstraintLayout: 자유도가 높아 위치 선정 등을 쉽게 할 수 있다. (체인 이용)

 

ConstraintLayout 위에 LinearLayout (vertical)을 추가한다.- 레이아웃 내용을 위아래로 쌓기 위해서이다.

 

LinearLayout (vertical) 위에 LinearLayout (horizontal)을 추가한다.

- LinearLayout (vertical) 위에 버튼 2개를 가로로 담아야 하기 때문이다.

 

LinearLayout (horizontal) 위에 Button 2개를 추가한다.

- 왼쪽 버튼의 id를 'btn_input', text'를 '입력'으로 설정한다.

- 오른쪽 버튼의 id를 'btn_result', text'를 '결과'로 설정한다.

=> 설정 후 Refactor를 이용해 적용해야한다.

 

LinearLayout (horizontal)의 layout_width, layout_height를 변경한다.

- 변경 전, 화면 전체를 차지하고 있기 때문이다. (버튼 아래에는 컨텐트를 넣어야한다.)

- match_parent: 자신을 포함하고 있는 부모 컴포넌트 크기에 맞춘다. (초기 설정)

- wrap_content: 자신을 포함하고 있는 내부(자식) 컴포넌트 크기에 맞춘다.

=> layout_height를 wrap_content로 변경한다.

 

LinearLayout (vertical) 위에 FrameLayout을 추가한다.


Activity vs Fragment 

Activity

- 화면 전체를 나타내는 객체이다.

- 실행 단위를 나타내는 Context 클래스의 하위 클래스이다.

- 독립적으로 자원을 분리하여 관리한다. 즉 Activity 끼리는 자원(메모리 등)을 공유하지 않는다.

 

Fragment

- 화면 일부를 나타내는 객체이다.

- 기본적으로 Activity 안에 존재한다.

- 자신을 포함한 Activity나 Fragment끼리 자원 공유가 가능하다.

- Fragment를 생성할 때는 클래스이기 때문에 이름을 대문자로 시작하는 것이 좋다.


InputFragment 생성하기

MainActivity 파일이 있는 곳에 Fragment(Blank)를 추가한다. (마우스 우측 클릭 -> New -> Fragment)

- 이름은 'InputFragment'로 설정한다.

 

fragment_input.xml의 Component Tree에서 frameLayout을 FrameLayout -> ConstraintLayout으로 변경한다.

- 내용의 위치를 자유롭게 하기 위해서 변경한다.

- Layout 이름과 Layout 종류가 일치하지 않아 헷갈리므로 id를 지운다. (이름: ConstraintLayout)

 

ConstraintLayout 위에 Plain Text를 추가한다.

- 상하좌우 체인을 모두 연결하여 위치를 가운데로 배치한다. (Constraint Layout의 강점)

- layout_width를 match_parent로 변경한다.

- text ('Name')을 지우고 id를 edit_text로 설정한다.


Fragment 코드 작성하기

inflate

- XML 레이아웃 파일을 메모리에 load한다.

- 레이아웃에 정의된 뷰 요소를 실제 뷰 객체로 만드는 프로세스이다.

(View = 사용자 UI의 구성요소 = Component)

=> XML에 있는 Component들을 실제 프로퍼티로 접근할 수 있다.

 

onCreate

- Fragment 객체가 처음 생성될 때 호출되는 함수이다. (초기 설정)

- UI 구성요소들을 생성하거나 레이아웃을 화면에 추가하는 작업은 수행하지 않는다.

- UI 관련 작업은 onCreateView 함수 또는 이후 단계에서 수행한다.

 

onCreateView

- Fragment가 화면에 표시될 때 호출되는 함수이다.

- 해당 Fragment의 사용자 인터페이스를 만들고 반환한다.

 

Fragment를 생성할 때, String 파라미터의 내용으로 text의 내용을 채우도록 InputFragment.kt를 수정한다.

- Fragment 레이아웃을 inflate하고 Binding 객체를 생성한다.

package com.example.mymultifragapplication

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.example.mymultifragapplication.databinding.FragmentInputBinding

class InputFragment(val str: String) : Fragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        val binding = FragmentInputBinding.inflate(inflater, container, false)
        // binding: InputFragment의 바인딩 객체

        binding.editText.setText(str)
        // InputFragment의 'editText'의 텍스트를 str로 설정

        return binding.root
        // binding의 최상위(루트) 뷰 반환
    }
}

MainActivity 코드 작성하기

onCreate

- Activity가 화면에 표시될 때 호출되는 함수이다.

- 해당 Activity의 사용자 인터페이스를 만들고 반환한다.

 

Transaction

- 하나의 꾸러미(작업 단위)를 생성하여 (begin Transaction) 여러 작업을 넣고, 동시에 실행 (commit) 한다.

 

MainActivity의 바인딩 객체를 생성하고, Transaction을 이용하여 Fragment를 배치한다.

package com.example.mymultifragapplication

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.mymultifragapplication.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding = ActivityMainBinding.inflate(layoutInflater)
        // binding: MainActivity의 바인딩 객체

        val tr = supportFragmentManager.beginTransaction()
        // Fragment를 관리하는 FragmentManager를 사용하여 Fragment 트랜잭션 시작

        tr.replace(binding.frmFrag.id, InputFragment("KAU"))
        // frmFrag id를 가진 fragment를 InputFragment로 교체

        tr.commit()
        // Fragment 트랜잭션을 확정하고, 변경 사항 적용

        setContentView(binding.root)
        // Activity 화면을 binding의 최상위(루트) 뷰로 설정
    }
}

영역 함수를 이용하여 MainActivity 코드 작성하기

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding = ActivityMainBinding.inflate(layoutInflater)
        // binding: MainActivity의 바인딩 객체

        // Fragment 설정
        supportFragmentManager.beginTransaction().run{
            replace(binding.frmFrag.id, InputFragment("KAU"))
            commit()
        }
        setContentView(binding.root)
    }
}

 

=> 그러나 Android Studio에서는 Fragment를 생성할 때, 생성자 파라미터에 아무것도 받지 않도록 권고한다.

예를 들어, 핸드폰을 가로로 돌렸다가 다시 세로로 돌리는 과정에서 자동으로 생성자 함수가 호출이 된다.

이 때, 파라미터에는 아무것도 없이 호출이 되기 때문에 문제가 발생한다.

 

=> Factoty Pattern을 사용하여 문제를 해결하자!


Factory Pattern을 이용하여 Fragment 작성하기

Factory Pattern

- 생성자를 이용하지 않고 Factory 객체의 함수를 통해 간접적으로 객체를 생성하는 방법

 

Bundle

- [key : value]의 쌍을 담는 바구니

- key: 문자열 

- value: 어떤 객체든 가능

- putString("key", value) 함수를 통해 담을 수 있다. 

- getString("key") 함수를 통해 value를 꺼낼 수 있다. (value 반환)

 

arguments

- 모든 Fragment 객체가 갖는 Bundle 프로퍼티이다.

 

< InputFragment.kt >

package com.example.mymultifragapplication

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.example.mymultifragapplication.databinding.FragmentInputBinding

class InputFragment() : Fragment() {

    var text: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        text = arguments?.getString("text")
        // argument 안의 "text" 키에 해당하는 value값을 text에 저장
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        val binding = FragmentInputBinding.inflate(inflater, container, false)
        // binding: InputFragment의 바인딩 객체

        binding.editText.setText(text)
        // InputFragment의 'editText'의 text값을 str로 설정

        return binding.root
        // binding의 최상위(루트) 뷰 반환
    }

    companion object {
        @JvmStatic
        fun newInstance(str: String): InputFragment {
            val frag = InputFragment()
            frag.arguments = Bundle()
            frag.arguments?.putString("text", str)

            return frag
        }

        /*
        @JvmStatic
        fun newInstance(str: String) =
            InputFragment().apply {
                arguments = Bundle().apply {
                    putString("text", str)

                }
            }
         */
    }
}

 

< MainActivity.kt >

package com.example.mymultifragapplication

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.mymultifragapplication.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding = ActivityMainBinding.inflate(layoutInflater)
        // binding: MainActivity의 바인딩 객체

        // Fragment 설정
        supportFragmentManager.beginTransaction().run{
            replace(binding.frmFrag.id, InputFragment.newInstance("KAU"))
            commit()
        }
        setContentView(binding.root)
    }
}

영역 함수를 이용하여 Fragment 작성하기

여러 흐름에서 프로퍼티를 null로 설정할 수 있기 때문에 프로퍼티는 스마트 캐스트가 되지 않는다.

=> let을 이용하여 널 안전성을 처리해야한다.

 

< InputFragment.kt >

package com.example.mymultifragapplication

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.example.mymultifragapplication.databinding.FragmentInputBinding

class InputFragment() : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        val binding = FragmentInputBinding.inflate(inflater, container, false)
        // binding: InputFragment의 바인딩 객체

        arguments?.let {
            binding.editText.setText(arguments?.getString("text"))
            // InputFragment의 'editText'의 text값을 argument의 "text" key를 가진 value로 설정
        }
        // arguments가 널이 아닐 때만 실행

        return binding.root
        // binding의 최상위(루트) 뷰 반환
    }

    companion object {
        @JvmStatic
        fun newInstance(str: String? = null) =
                InputFragment().apply {
                    arguments = Bundle().apply {
                        str?.let {
                            putString("text", it)
                        }
                    }
                }
    }
}

 

< MainActivity.kt >

package com.example.mymultifragapplication

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.mymultifragapplication.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding = ActivityMainBinding.inflate(layoutInflater)
        // binding: MainActivity의 바인딩 객체

        // Fragment 설정
        supportFragmentManager.beginTransaction().run{
            replace(binding.frmFrag.id, InputFragment.newInstance())
            commit()
        }
        setContentView(binding.root)
    }
}

ResultFragment 생성하기

MainActivity 파일이 있는 곳에 Fragment(Blank)를 추가한다. (마우스 우측 클릭 -> New -> Fragment)

- 이름은 'ResultFragment'로 설정한다.

 

fragment_Result.xml의 Component Tree에서 frameLayout을 FrameLayout -> ConstraintLayout으로 변경한다.

 

ConstraintLayout 위에 TextView를 추가한다. (텍스트를 보여주기만 한다.)

- 상하좌우 체인을 모두 연결하여 위치를 가운데로 배치한다.

- text ('textview')을 지우고 id를 txt_result로 설정한다.

- Declared Attribute의 +버튼을 눌러서 size를 30sp로 설정한다.

 

< ResultFragment.kt >

package com.example.mymultifragapplication

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.example.mymultifragapplication.databinding.FragmentResultBinding

class ResultFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val binding = FragmentResultBinding.inflate(inflater, container, false).apply {
            arguments?.let {
                txtResult.text = it.getInt("kor_count").toString() // String형으로 변환해야 볼 수 있다.
                // ResultFragment의 'txtResult'의 text값을 argument의 "kor_count" key를 가진 value로 설정
            }
        }
        return binding.root
    }

    companion object {
        @JvmStatic
        fun newInstance(count: Int) =
            ResultFragment().apply {
                arguments = Bundle().apply {
                    putInt("kor_count", count )
                }
            }
    }
}

 


화면 전환 버튼 설정하기

< InputFragment.kt >

package com.example.mymultifragapplication

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.setFragmentResult
import com.example.mymultifragapplication.databinding.FragmentInputBinding

class InputFragment() : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        val binding = FragmentInputBinding.inflate(inflater, container, false)
        // binding: InputFragment의 바인딩 객체

        // arguments가 널이 아닐 때만 실행
        arguments?.let {
            binding.editText.setText(it.getString("text"))
            // InputFragment의 'editText'의 text값을 argument의 "text" key를 가진 value로 설정
        }

        // editText에 텍스트가 입력될 때 발생하는 변경사항을 감지 -> 코드 실행
        binding.editText.addTextChangedListener {
            val resBundle = Bundle().apply {
                putString("input", binding.editText.text.toString())
            }
            // Fragment 간 데이터 송신 -> MainActivity에서 setFragmentResultListener를 통해 수신
            setFragmentResult("input_text", resBundle)
        }

        return binding.root
        // binding의 최상위(루트) 뷰 반환
    }

    companion object {
        @JvmStatic
        fun newInstance(str: String? = null) =
                InputFragment().apply {
                    arguments = Bundle().apply {
                        str?.let {
                            putString("text", it)
                        }
                    }
                }
    }
}

 

< MainActivity.kt >

package com.example.mymultifragapplication

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.fragment.app.Fragment
import com.example.mymultifragapplication.databinding.ActivityMainBinding

// String의 확장 프로퍼티: 한글 개수
val String.numOfKoreanCharacters: Int
    get() {
        var count = 0
        for (i in 0 until length) {
            if (this[i] >= 0xAC00.toChar() && this[i] <= 0xD7AF.toChar()){
                count += 1
            }
        }
        return count
    }

class MainActivity : AppCompatActivity() {
    var text: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // MainActivity의 바인딩 객체
        val binding = ActivityMainBinding.inflate(layoutInflater)

        // Fragment 설정 함수
        fun replaceFragment(frag: Fragment) {
            supportFragmentManager.beginTransaction().run{
                replace(binding.frmFrag.id, frag)
                commit()
            }
        }

        binding.run {
            // btnInput 버튼이 눌렸을 때 실행
            btnInput.setOnClickListener {
                replaceFragment(InputFragment.newInstance())
            }

            // btnResult 버튼이 눌렸을 때 실행
            btnResult.setOnClickListener {
                replaceFragment(ResultFragment.newInstance(text?.numOfKoreanCharacters ?: 0))
            }
        }

        // // Fragment 간 데이터 수신
        supportFragmentManager.setFragmentResultListener("input_text", this) { _, bundle ->
            text = bundle.getString("input")
        }

        replaceFragment(InputFragment.newInstance())

        setContentView(binding.root)
    }
}

 

 

< ResultFragment.kt >

package com.example.mymultifragapplication

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.example.mymultifragapplication.databinding.FragmentResultBinding

class ResultFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val binding = FragmentResultBinding.inflate(inflater, container, false).apply {
            arguments?.let {
                txtResult.text = it.getInt("kor_count").toString() // String형으로 변환해야 볼 수 있다.
                // ResultFragment의 'txtResult'의 text값을 argument의 "kor_count" key를 가진 value로 설정
            }
        }
        return binding.root
    }

    companion object {
        @JvmStatic
        fun newInstance(count: Int) =
            ResultFragment().apply {
                arguments = Bundle().apply {
                    putInt("kor_count", count )
                }
            }
    }
}

'Study > Kotlin Study' 카테고리의 다른 글

[Kotlin] 하위 클래스 초기화  (0) 2023.10.23
[Kotlin] 하위 클래스 선언  (2) 2023.10.23
[Kotlin] 영역 함수  (0) 2023.10.09
[Kotlin] 확장  (0) 2023.10.09
[Kotlin] 함수형 프로그래밍  (0) 2023.09.22