Android Fragments Tutorial: An Introduction with Kotlin
In this Android Fragments with Kotlin tutorial you will learn the fundamental concepts of fragments while creating an app that displays dogs breeds. By Aaqib Hussain.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Android Fragments Tutorial: An Introduction with Kotlin
30 mins
- Getting Started With Android Fragments
- Android Fragment Lifecycle
- The V4 Support Library
- Creating a Fragment
- Using a factory method
- Adding a Fragment
- Adding a Fragment Dynamically
- Data Binding
- Communicating With the Activity
- Fragment Arguments and Transactions
- Binding Adapters
- Navigating the Fragment Back Stack
- Where To Go From Here?
Communicating With the Activity
Even though fragments attach to an activity, they don’t necessarily all talk to one another without some further encouragement from you.
For El Dogo, you’ll need DogListFragment
to let MainActivity
know when the user has made a selection so that DogDetailsFragment
can display the selection.
To start, open DogListFragment.kt and add the following Kotlin interface at the bottom of the class:
interface OnDogSelected {
fun onDogSelected(dogModel: DogModel)
}
This defines a listener interface for the activity to listen to the fragment. The activity will implement this interface and the fragment will invoke the onDogSelected()
when the user selects an item, passing the selection to the activity.
Add this new field below the existing ones in DogListFragment
:
private lateinit var listener: OnDogSelected
This field is a reference to the fragment’s listener, which will be the activity.
In onAttach()
, add the following directly below super.onAttach(context)
:
if (context is OnDogSelected) {
listener = context
} else {
throw ClassCastException(
context.toString() + " must implement OnDogSelected.")
}
This initializes the listener reference. Wait until onAttach()
to ensure that the fragment actually attached itself. Then verify that the activity implements the OnDogSelected
interface via is
.
If it doesn’t, it throws an exception since you can’t proceed. If it does, set the activity as the listener
for DogListFragment
.
if (context != null)
line in onAttach()
if you like.
Okay, I lied a little: The DogListAdapter
doesn’t have everything you need! In the onBindViewHolder()
method in DogListAdapter
, add this code to the bottom.
viewHolder.itemView.setOnClickListener {
listener.onDogSelected(dog)
}
This adds a View.OnClickListener
to each dog breed so that it invokes the callback on the listener, the activity, to pass along the selection.
Open MainActivity.kt and update the class definition to implement OnDogSelected
:
class MainActivity : AppCompatActivity(),
DogListFragment.OnDogSelected {
You will get an error asking you to make MainActivity
abstract or implement the abstract method onDogSelected(dogModel: DogModel)
. Don’t fret yet, you’ll resolve it soon.
This code specifies that MainActivity
is an implementation of the OnDogSelected
interface.
For now, you’ll show a toast to verify that the code works. Add the following import below the existing imports so that you can use toasts:
import android.widget.Toast
Then add the following method below onCreate()
:
override fun onDogSelected(dogModel: DogModel) {
Toast.makeText(this, "Hey, you selected " + dogModel.name + "!",
Toast.LENGTH_SHORT).show()
}
The error is gone! Build and run. Once the app launches, click one of the dog breed. You should see a toast message naming the clicked item:
Now you’ve got the activity and its fragments talking. You’re like a master digital diplomat!
Fragment Arguments and Transactions
Currently, DogDetailsFragment
displays a static Drawable
and set of Strings
. But what if you want it to display the user’s selection?
First, replace the entire view in fragment_dog_details.xml
with:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="dogModel"
type="com.raywenderlich.android.eldogo.DogModel" />
</data>
<ScrollView xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
tools:ignore="RtlHardcoded">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/name"
style="@style/TextAppearance.AppCompat.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dog_detail_name_margin_top"
android:layout_marginBottom="0dp"
android:text="@{dogModel.name}" />
<ImageView
android:id="@+id/dog_image"
imageResource="@{dogModel.imageResId}"
android:layout_width="wrap_content"
android:layout_height="@dimen/dog_detail_image_size"
android:layout_marginTop="@dimen/dog_detail_image_margin_vertical"
android:layout_marginBottom="@dimen/dog_detail_image_margin_vertical"
android:adjustViewBounds="true"
android:contentDescription="@null"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/description"
style="@style/TextAppearance.AppCompat.Body1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="@dimen/dog_detail_description_margin_left"
android:layout_marginTop="0dp"
android:layout_marginRight="@dimen/dog_detail_description_margin_right"
android:layout_marginBottom="@dimen/dog_detail_description_margin_bottom"
android:autoLink="web"
android:text="@{dogModel.text}" />
</LinearLayout>
</ScrollView>
</layout>
This is almost the same as the layout was before except with data binding added. At the top you’ll see that you’ve added a variable for our DogModel. The text for name and description is bound to the variables of the same name in the DogModel object. Then, you’re using this variable to set values on the views.
Binding Adapters
On the ImageView
for the dog image you’ll notice the following tag:
imageResource="@{dogModel.imageResId}"
This corresponds to a binding adapter in the DataBindingAdapters.kt
file.
@BindingAdapter("android:src")
fun setImageResoruce(imageView: ImageView, resource: Int) {
imageView.setImageResource(resource)
}
A binding adapter allows you to perform actions on an element which are not supported by default data binding. In your case, you are storing a resource integer for the image to display, but data binding does not provide a default way to display an image from an ID.
To fix that, you have a BindingAdapter that takes a reference to the object from which it was invoked, along with a parameter. It uses that to call setImageResource
on the imageView
that displays the image of the dog.
Now that your view is set up, replace newInstance()
in DogDetailsFragment
with the code shown below:
private const val DOGMODEL = "model"
fun newInstance(dogModel: DogModel): DogDetailsFragment {
val args = Bundle()
args.putSerializable(DOGMODEL, dogModel)
val fragment = DogDetailsFragment()
fragment.arguments = args
return fragment
}
A fragment can take initialization parameters through its arguments, which you access via the arguments
property. The arguments are actually a Bundle
that stores them as key-value pairs, like the Bundle
in Activity.onSaveInstanceState
or the Activity extras from intent.extras
.
You create and populate the arguments’ Bundle
, set the arguments
and, when you need the values later, you reference arguments
property to retrieve them.
As you learned earlier, when a fragment is re-created, the default empty constructor is used— no parameters for you.
Because the fragment can recall initial parameters from its persisted arguments, you can utilize them in the re-creation. The above code also stores information about the selected breed in the DogDetailsFragment
arguments.
Add the following import to the top of DogDetailsFragment.kt:
import com.raywenderlich.android.eldogo.databinding.FragmentDogDetailsBinding
FragmentDogDetailsBinding
to be available
Now, replace the contents of onCreateView() with the following:
// 1
val fragmentDogDetailsBinding =
FragmentDogDetailsBinding.inflate(inflater, container, false)
// 2
val model = arguments!!.getSerializable(DOGMODEL) as DogModel
// 3
fragmentDogDetailsBinding.dogModel = model
model.text = String.format(getString(R.string.description_format),
model.description, model.url)
return fragmentDogDetailsBinding.root
Breaking it down:
- Inflate the view using
FragmentDogDetailsBinding
. Since you want to dynamically populate the UI of theDogDetailsFragment
with the selection, you grab the reference to theFragmentDogDetailsBinding
. - Get the
DogModel
from thearguments
. -
Next, you bind the view
dogModel
with theDogModel
that you’ve passed toDogDetailsFragment
.
Finally, you need to create and display a DogDetailsFragment
when a user clicks an item, instead of showing a toast. Open MainActivity and replace the logic inside onDogSelected
with:
// 1
val detailsFragment =
DogDetailsFragment.newInstance(dogModel)
supportFragmentManager
.beginTransaction()
// 2
.replace(R.id.root_layout, detailsFragment, "dogDetails")
// 3
.addToBackStack(null)
.commit()
You’ll find this code is similar to your first transaction which added the list to MainActivity
, but there are also some notable differences. In this code you:
- Create a fragment instance that includes some nifty parameters.
- Call
replace()
, instead ofadd
, which removes the fragment currently in the container and then adds the new Fragment. - Call another new friend: the
addToBackStack()
ofFragmentTransaction
. Fragments have a back stack, or history, like Activities.