[Android] create fragment


Writing time : 2021-07-26 09:23:36

Fragment Features

Fragment can be included as a part of Activity as well as View.
And the difference is that View does not have the same lifecycle as Activity, but Fragment has lifecycle.


Use of Fragments

Fragment is included as a part of Activity like View, but it is used when life cycle processing is required like Activity.

For reference, the following is a list of classes that inherit Fragment.
DialogFragment, ListFragment, PreferenceFragment, WebViewFragment

Creating a Fragment with XML

fragment_hello.xml

Create layout xml file to be used in Fragment.

<?xml version="1.0" encoding="utf-8"?>  
<FrameLayout  
    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">  
  
    <TextView  
        android:id="@+id/textMessage"  
        android:layout_width="match_parent"  
        android:layout_height="match_parent"  
        android:text="Hello"  
        android:gravity="center"  
        android:textSize="20sp"  
        tools:ignore="HardcodedText" />  
</FrameLayout>  

XmlFragment.kt

Add the Fragment class containing the xml file.

class XmlFragment: Fragment() {  
    override fun onCreateView(  
        inflater: LayoutInflater,  
        container: ViewGroup?,  
        savedInstanceState: Bundle?  
    ): View? {  
        return inflater.inflate(R.layout.fragment_hello, container, false)  
    }  
  
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {  
        super.onViewCreated(view, savedInstanceState)  
    }  
}  

activityfragmentxml.xml

Add FragmentContainerView to the xml file and add the package name and class name of the fragment to the android:name attribute.

<androidx.fragment.app.FragmentContainerView  
    xmlns:android="http://schemas.android.com/apk/res/android"  
    android:id="@+id/fragment_container_view"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent"  
    android:name="com.devez.app.android.fragment.xml.XmlFragment" />  

XmlFragmentActivit.kt

If you create an Activity and inflate the xml file containing the Fragment, the Fragment is added to the Activity.

class XmlFragmentActivity: AppCompatActivity() {  
    override fun onCreate(savedInstanceState: Bundle?) {  
        super.onCreate(savedInstanceState)  
        setContentView(R.layout.activity_fragment_xml)  
    }  
}  


Creating Fragments with Code

The following is how to create a Fragment with code.

fragment_code.xml

Create layout xml file to be used in Fragment.

<?xml version="1.0" encoding="utf-8"?>  
<FrameLayout  
    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">  
  
    <TextView  
        android:id="@+id/textMessage"  
        android:layout_width="match_parent"  
        android:layout_height="match_parent"  
        android:gravity="center"  
        android:textSize="20sp"  
        tools:ignore="HardcodedText" />  
</FrameLayout>  

CodeFragment.kt

It is the source code of the Fragment to be added as code to the Activity.

class CodeFragment: Fragment() {  
    private var message: String? = null  
    override fun onCreate(savedInstanceState: Bundle?) {  
        super.onCreate(savedInstanceState)  
        message = arguments?.getString(MESSAGE)  
    }  
  
    override fun onCreateView(  
        inflater: LayoutInflater,  
        container: ViewGroup?,  
        savedInstanceState: Bundle?  
    ): View? {  
        return inflater.inflate(R.layout.fragment_code, container, false)  
    }  
  
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {  
        super.onViewCreated(view, savedInstanceState)  
        view.findViewById<TextView>(R.id.textMessage).text = message  
    }  
  
    companion object{  
        const val MESSAGE = "message"  
        fun newInstance(message: String): Fragment {  
            val fragment = CodeFragment()  
            fragment.arguments = Bundle().apply {  
                putString(MESSAGE, message)  
            }  
            return fragment  
        }  
    }  
}  

activityfragmentcode.xml

Add the FragmentContainerView layout to which the Fragment will be added to the xml file.
Fragment is added to FragmentContainerView.

<androidx.fragment.app.FragmentContainerView  
    xmlns:android="http://schemas.android.com/apk/res/android"  
    android:id="@+id/fragment_container_view"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent"/>  

CodeFragmentActivity.kt

Create an Activity and inflate the xml file containing the Fragment.
It is an empty Activity that has not yet added a Fragment.

class CodeFragmentActivity: AppCompatActivity(){  
  
    override fun onCreate(savedInstanceState: Bundle?) {  
        super.onCreate(savedInstanceState)  
        setContentView(R.layout.activity_fragment_code)  
    }  

Add Fragment

Add Fragment to Activity.

val newFragment = CodeFragment.newInstance(getNextCount())  
val transaction = supportFragmentManager.beginTransaction()  
transaction.replace(R.id.fragment_container, newFragment, "myTag")  
transaction.addToBackStack(null)  
transaction.commit()  


More about Fragments

We have seen how to add a Fragment with xml and code.
Next, let's look at a few more items needed to work with Fragments.

1. onCreateView, onViewCreated

onCreateView is a method that creates a View, and onViewCreated is a method to which code necessary to process the created View is added.

onCreateView is responsible for creating and returning a View.
You can access the View in onCreateView, but since the View has not yet been created and added to the Fragment, in onCreateView, only add the code that creates and returns the View.

override fun onCreateView(  
    inflater: LayoutInflater,  
    container: ViewGroup?,  
    savedInstanceState: Bundle?  
): View? {  
    return inflater.inflate(R.layout.fragment_code, container, false)  
}  


In onViewCreated, the View created in onCreateView is passed as an argument.
Since the View is completely created, the code to control and process the View and its child Views is added to the onCreateView method.

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {  
    super.onViewCreated(view, savedInstanceState)  
    textView = view.findViewById(R.id.textMessage)  
    if(message != null){  
        textView.text = message  
    }  
}  
  

2. replace, add

In the source code written above, the replace method of the FragmentTransaction class was used.
The replace method removes the added Fragment and adds a new Fragment if another Fragment has already been added to the FragmentContainer.

When the add method is used, a new Fragment is added on top of the already added Fragment.

You can put a tag name as the last argument of the replace method, and you can pass null as the tag name is an optional argument.
The advantage of passing the tag name as an argument is that the added Fragment can be found with the findFragmentByTag method of the FragmentManager class.

3. newInstance

How do I pass the information to be passed when creating a Fragment?

For a general class, there is a way to pass the necessary information to the constructor.

However, in the case of a Fragment, the information passed to the constructor may disappear when the Fragment is regenerated.
When the system thinks that memory management is necessary, the fragment is destroyed, and when memory is freed, the fragment is created again. This is because the default constructor is used when re-creating it.

If the constructor of a Fragment is overloaded, the following error occurs when the Fragment is re-created.

Error message: Fragment$InstantiationException: Unable to instantiate fragment make sure class name exists, is public, and has an empty constructor that is public  


The error occurs because the system cannot create a Fragment using the default constructor if the Fragment's constructor is overloaded.
If you overload a constructor in Java, you know that you cannot create an object with the default constructor.
So, although it is not recommended, if you need to overload a constructor, you should add an empty default constructor to prevent an error from occurring.

The way to pass the necessary information to the Fragment without overloading the constructor is to use the Bundle class.
Below is the code to create a Fragment, store a message in the Bundle object, and set it as arguments of the Fragment.

val fragment = CodeFragment()  
fragment.arguments = Bundle().apply {  
    putString("message", message)  
}  
return fragment  


The data passed to the Bundle is passed as an argument to the onCreate method of the Fragment.
Even when a Fragment is destroyed and re-created by the system, the data stored in the Bundle does not disappear and is passed as an argument to the onCreate method.

If you use Bundle, the data is maintained even if the Fragment is re-created without overloading the constructor, but the code that creates the Fragment will increase.
Whenever you create a Fragment, create a Fragment and Bundle object, store data in the Bundle object, and set it as arguments of the Fragment.
If fragments are used in many places, there is a disadvantage of repeating the same code, and this disadvantage can be solved by using a factory method.

The factory method name is conventionally newInstace.
The newInstace method returns the created Fragment.

companion object{  
    const val MESSAGE = "message"  
    fun newInstance(message: String): Fragment {  
        val fragment = CodeFragment()  
        fragment.arguments = Bundle().apply {  
            putString(MESSAGE, message)  
        }  
        return fragment  
    }  
}  


You can create a Fragment object to which the necessary information is passed by calling the newInstace method as follows in the place where the Fragment is created.

val newFragment = CodeFragment.newInstance("Hello")  

4. addBackTrace

When adding or removing a Fragment to or from a FramentContainer, the beginTransaction method should be called first and the commit method should be called last.
Let's look at the code we wrote above again.

val newFragment = CodeFragment.newInstance(getNextCount())  
val transaction = supportFragmentManager.beginTransaction()  
transaction.replace(R.id.fragment_container, newFragment, "myTag")  
transaction.addToBackStack(null)  
transaction.commit()  


The code written between the beginTransaction method and the commit method becomes one transaction.
The addToBackStack method is optional, and if the addToBackStack method is called before the commit method is called, the transaction is rolled back when the back key is pressed.

The argument of the addToBackStack method is optional, and a null value may be passed.
The advantage when the argument of the addToBackStack method is used is that the state can be returned to a specific point in time by using the popBackStack method of the FragmentManager.

If you do not want to rollback the transaction when the back key is pressed, do not use addToBackStack as shown in the code below.

val newFragment = CodeFragment.newInstance(getNextCount())  
val transaction = supportFragmentManager.beginTransaction()  
transaction.replace(R.id.fragment_container, newFragment, "myTag")  
transaction.commit()  


class ExampleActivity : AppCompatActivity(R.layout.example_activity) {  
    override fun onCreate(savedInstanceState: Bundle?) {  
        super.onCreate(savedInstanceState)  
        if (savedInstanceState == null) {  
            supportFragmentManager.commit {  
                setReorderingAllowed(true)  
                add<ExampleFragment>(R.id.fragment_container_view)  
            }  
        }  
    }  
}  

5. Briefly express beginTransaction and commit codes with fragment-ktx

The order of adding Fragment to Activity is as follows.

1. Fragment 생성  
2. FragmentActivity의 getSupportFragmentManager 메소드를 호출해서 FragmentManager 객체를 저장  
3. FragmentManager 클래스의 beginTransaction 함수호출  
4. replace, add, remove 같은 Fragment 처리  
5. FragmentManager 클래스의 commit 함수 호출  


Here, 1, 2, 3, and 5 are repetitive tasks.
Only the part processed by Fragment No. 4 is changed, and most of it is processed by calling the method of the FragmentManager class.


If you include fragment-ktx, you can omit the repeated part as follows and pass only the part that processes the Fragment as a lambda expression.

supportFragmentManager.commit {  
    setReorderingAllowed(true)  
    replace<CodeFragment>(R.id.fragment_container)  
}  


fragment-ktx can be included in Gradle like this:

dependencies {  
    val fragment_version = "1.3.6"  
  
    // Java language implementation  
    implementation("androidx.fragment:fragment:$fragment_version")  
    // Kotlin  
    implementation("androidx.fragment:fragment-ktx:$fragment_version")  
}  


Let's compare the code with and without fragment-ktx

deprecated fragment-ktx

private fun replaceFragment(){  
    val newFragment = CodeFragment.newInstance(getNextCount())  
    val transaction = supportFragmentManager.beginTransaction()  
    transaction.replace(R.id.fragment_container, newFragment, "myTag")  
    transaction.addToBackStack(null)  
    transaction.commit()  
}  

Using fragment-ktx

private fun replaceFragment(){  
    supportFragmentManager.commit {  
        addToBackStack(null)  
        replace<CodeFragment>(R.id.fragment_container, "myTag", args = null)  
    }  
}  

6. setReorderingAllowed(true)

setReorderingAllowed optimizes setReorderingAllowed of a Fragment.
If multiple Transactions are executed at the same time, there may be meaningless actions. Setting the setReorderingAllowed method to true prevents unnecessary actions from being processed.

The following is the Fragment add sample code that calls the setReorderingAllowed method.

supportFragmentManager.commit {  
    setReorderingAllowed(true)  
    add<CodeFragment>(R.id.fragment_container, args = bundle)  
}