Fragments allow us to reuse elements of code. In this tutorial, we will adapt the RecyclerView application, so that when used in landscape orientation, it can display both the list and the fields for adding a new item, as shown here:
tools:context
attribute of the ConstraintLayout, so it reads as follows:
tools:context=".AddItemFragment"
@layout/fragment_add_item
. Be aware that this is only for the purpose of the designer preview. It has no effect on the applicationfrAddItem
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
binding = FragmentAddItemBinding.inflate(inflater)
return binding.root
onCreate
method, move the methods, and the inner EnterHandler class, from the AddItemActivity class to the AddItemFragment class. When you paste the methods and inner class, you will be prompted to import various elements. Ensure that the ones that start kotlinx
are not selectedsetResult
method with be showing an error. This is because setResult
is a method that belongs to the Activity class. We can amend the error by getting a reference to the underlying activity and calling it using that, modify the line of code so that it reads as follows:
activity?.setResult(Activity.RESULT_OK, intent)
finish
so it is called on the activity:
activity?.finish()
Note that in both cases, these are safe calls (indicated by the ?
. In the event that there was no Activity, the methods would not be calledonViewCreated
method (which will need to be added), rather than onCreateView
. Override the the onActivityCreated method in the Fragment
setContentView(R.layout.activity_add_item)
, move the remaining code from onCreate
in the Activity, to onActivityCreated
in the Fragment. If prompted, do not import any of the elements starting with kotlinx
activity?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
Note the extra ?
after the call to getWindow()
The fragment code is not as reusable as it might be. It creates an intent, finishes the current activity, and passes data back to the previous activity. When we add the Fragment to the landscape orientation of the MainActivity, we will not want this behaviour, instead we would want to update the list directly. One approach to doing this could be to write some logic to check the type of the parent activity and adjust the behaviour accordingly, however that defeats the purpose of trying to make the fragment as reusable as possible. Really what it should do is be able to say "I'm done, here is the data" to any Activity, or indeed another Fragment in which it is nested, to do this, it needs to be designed in a way that will allow it to do that without knowing about where it is. This also helps it conform to the Single Responsibility Principle, as it's only responsibility is to create a new ShoppingListItem instance.
To implement this sort of behaviour, we can create an interface inside the Fragment, and in that we will declare a method which takes a ShoppingListItem as a parameter. Anything implementing that interface would need to implement the method. We'll also create a nullable property in the AddItemFragment class of the type of the interface. When the fragment is created (for example by an activity) if this other class implements the interface, we will set it as that property. When the Fragment has a ShoppingListItem it can make a safe call to the property and pass the ShoppingListItem. Although this probably sounds confusing, it should make more sense once you write the code.
interface AddItemFragmentListener {
}
fun onItemAdded(item: ShoppingListItem)
private var addItemListener : AddItemFragmentListener? = null
Note how this will by null by defaultonAttach(context: Context)
method and add the following code inside (after the call to super):
if (context is AddItemFragmentListener) {
addItemListener = context
}
This checks if whatever the Fragment is being attached to implements the interface, and, if it does, sets it to be the addItemListenerclass AddItemActivity : AppCompatActivity(), AddItemFragment.AddItemFragmentListener {
onItemAdded
methodonItemAdded
method. You may need to remove a call to activity
prefixing the putExtra
method.addItemListener?.onItemAdded(product)
Eventually we will create a landscape layout version of the activity_main.xml file, which will show both the RecyclerView and the AddItemFragment side by side. To simplify things, we will move the RecyclerView into a fragment also
xmlns:app="http://schemas.android.com/apk/res-auto"
constraintTop_toTopOf="parent"
handleDragging()
method from MainActivity to the new FragmentDragCallback
class and its methods from Main Activity to the ShopListFragmentlayoutManager
and adapter
properties from Main Activity to ShopListFragmentonViewCreated
in ShopListFragment.this
(i.e. the Fragment) is being passed. Modify this
to read activity
handleDragging()
into onViewCreated
private
access modifier from the declaration of adapter
frList
private lateinit var listFrag: ShopListFragment
onCreate()
add code to instantiate the above instance variable with the fragment, casting it as a ShopListFragment as follows:
listFrag = supportFragmentManager.findFragmentById(R.id.frList) as ShopListFragment
adapter
(probably highlighted in red) and prefix each call with listFrag.
loadList()
cannot take place until the adapter is initialised, but this now happens after the activity is created. To solve this problem, override the onStart()
method in MainActivity and move it to thereloadList
is called when we return to the activity from the AddItemActivity, and replaces the list with the one from disk!loadList()
method to the ShopListFragment.openFileInput()
method is part of Activity, so prefix this method call with activity?.
(Note this is a safe call, but we should be confident that Activity exists)listFrag
, as we are now in that classmakeToast
, replace the call to this
with activity
fileInputStream
is now a nullable object, modify the call to close()
on it, to be a safe call, as follows:fileInputStream?.close()
loadList()
in the onViewCreated
methodonStart()
method from MainActivityloadList()
method has been added, move the saveList()
method to the ShopListFragmentsaveList()
from MainActivitysaveList()
in the ShopListFragment's onSaveInstanceState()
and onPause()
methodsSo that we can see the real power of fragments, we want to create a landscape version which will display both the list and the add item fragment at the same time. to start, we need to create a landscape specific layout.
<?xml version="1.0" encoding="utf-8" ?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="?android:attr/actionBarSize"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
android:elevation="4dp"
android:theme="?attr/actionBarTheme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout android:layout_width="0dp"
android:layout_height="0dp"
android:baselineAligned="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/frList"
android:name="com.tinyappco.shoplist.ShopListFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
tools:layout="@layout/fragment_shop_list"
android:layout_weight="1" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/frAddItem"
android:name="com.tinyappco.shoplist.AddItemFragment"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
tools:layout="@layout/fragment_shop_list"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE){
binding.toolbar.menu.findItem(R.id.menu_insert).isVisible = false
}
fun Activity.isLandscape() : Boolean {
return this.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
}
In the same way the we made the AddItemActivity implement the AddItemFragmentListener interface, we can do the same for the MainActivity.
onItemAdded
methodlistFrag.adapter.addItem(item)
onItemAdded
method:
val manager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
manager.hideSoftInputFromWindow(window.decorView.rootView.windowToken, 0)
android:imeOptions="flagNoExtractUi"
You can download a version of the ShopList app that supports rotation from GitHub
The add item fragment could be used for collecting a quantity of any item (not just a shopping item). Modify the code so it is more generic. This means that it shouldn't depend on the ShoppingListItem class, and that the placeholder text from the item name should be customisable
We have had to expose the adapter outside the fragment. This is unwise, it should be encapsulated, refactor the code so that the adapter is private - you will need to add methods to the fragment to handle to changes to the adapter
Add functionality, such that when an item is long-pressed, and edit dialogue appears and allows them to change the item and count. As a bonus, consider handling changes to the plurality of the item if the count is changed to, or from, a value of one. Watch our for products like potatoes (or sheep).