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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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.

Note: Now you can remove the 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:

El Dogo app with toast displayed

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
Update note: You may need to rebuild the project for 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:

  1. Inflate the view using FragmentDogDetailsBinding. Since you want to dynamically populate the UI of the DogDetailsFragment with the selection, you grab the reference to the FragmentDogDetailsBinding.
  2. Get the DogModel from the arguments.
  3. Next, you bind the view dogModel with the DogModel that you’ve passed to DogDetailsFragment.

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:

  1. Create a fragment instance that includes some nifty parameters.
  2. Call replace(), instead of add, which removes the fragment currently in the container and then adds the new Fragment.
  3. Call another new friend: the addToBackStack() of FragmentTransaction. Fragments have a back stack, or history, like Activities.