In this tutorial you will add the functionality which enables the user to navigate back to any position in the backstack. This will be achieved by adding breadcrumbs. If you don’t know about breadcrumbs please read about it here before continuing with the article.
In short, breadcrumbs are trail of of paths/pages visited by the user. Almost all websites provide navigation using breadcrumbs. Below is an example (image from EggHead).
In our Kotlin File Explorer application we will add breadcrumbs in the app bar using a RecyclerView.
Setting up RecyclerView for Breadcrumbs
Let’s first set up the item view for the RecyclerView. I have named the file item_recycler_breadcrumb.xml. A single item will look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.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="wrap_content" android:layout_height="wrap_content" android:background="?selectableItemBackgroundBorderless" android:padding="8dp"> <ImageView android:id="@+id/arrowTextView" android:layout_width="20dp" android:layout_height="20dp" android:src="@drawable/ic_chevron_right_black_24dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toTopOf="parent" android:tint="@color/grey"/> <TextView android:id="@+id/nameTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="8dp" android:layout_marginStart="4dp" android:textSize="20sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toRightOf="@id/arrowTextView" app:layout_constraintTop_toTopOf="parent" tools:text="Pictures" /> </android.support.constraint.ConstraintLayout> |
Create a new class named BreadcrumbRecyclerAdapter that extends RecyclerView.Adapter. The data for this RecyclerAdapter will be a list of FileModel. As the user clicks and navigates through the file system, we will keep updating the FileModel list. All the other methods inside this class are basic for a RecyclerView.Adapter.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
class BreadcrumbRecyclerAdapter : RecyclerView.Adapter<BreadcrumbRecyclerAdapter.ViewHolder>() { var onItemClickListener: ((FileModel) -> Unit)? = null var files = listOf<FileModel>() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_recycler_breadcrumb, parent, false) return ViewHolder(view) } override fun getItemCount() = files.size override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bindView(position) fun updateData(files: List<FileModel>) { this.files = files notifyDataSetChanged() } inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener { init { itemView.setOnClickListener(this) } override fun onClick(v: View?) { onItemClickListener?.invoke(files[adapterPosition]) } fun bindView(position: Int) { val file = files[position] itemView.nameTextView.text = file.name } } } |
In the AppBar of activity_main.xml add a RecyclerView.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout 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:id="@+id/coordinatorLayout" android:layout_height="match_parent" android:background="@color/colorPrimaryDark" tools:context=".main.MainActivity"> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:title="@string/app_name" app:titleTextColor="@color/grey"> </android.support.v7.widget.Toolbar> <android.support.v7.widget.RecyclerView android:id="@+id/breadcrumbRecyclerView" android:layout_width="match_parent" android:layout_height="wrap_content"> </android.support.v7.widget.RecyclerView> </android.support.design.widget.AppBarLayout> <FrameLayout app:layout_behavior="@string/appbar_scrolling_view_behavior" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/container"> </FrameLayout> </android.support.design.widget.CoordinatorLayout> |
Backstack Manager
Before going ahead and wiring up the RecyclerAdapter with the RecyclerView, lets do some extra work that will keep our MainActivity clean. Now we can directly add the list of FileModel to the adapter and keep updating it as user moves too and fro, but that will be too much mess in MainActivity and as you know, mess always grows! So what we are going to do is maintain a stack of the FileModel the user has visited. So if the user taps on a folder, we will push the corresponding FileModel on top of the stack.
Create a class named BackStackManager.
1 2 |
class BackStackManager { } |
Add a mutable list of FileModel which we will use as our stack.
1 2 3 |
class BackStackManager { private var files = mutableListOf<FileModel>() } |
Let’s add method to push and pop from the stack.
1 2 3 4 5 6 7 8 9 10 11 12 |
class BackStackManager { private var files = mutableListOf<FileModel>() fun addToStack(fileModel: FileModel) { files.add(fileModel) } fun popFromStack() { if (files.isNotEmpty()) files.removeAt(files.size - 1) } } |
We will add a method to get the top of the stack, not pop it!, just get what is at the top of the stack.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class BackStackManager { private var files = mutableListOf<FileModel>() val top: FileModel get() = files[files.size - 1] fun addToStack(fileModel: FileModel) { files.add(fileModel) } fun popFromStack() { if (files.isNotEmpty()) files.removeAt(files.size - 1) } } |
Our BackStackManger class is pretty straight forward till now. Let’s add a little more complex, yet quite simple function to it. A function that removes keeps popping items from the stack till a particular point.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class BackStackManager { private var files = mutableListOf<FileModel>() val top: FileModel get() = files[files.size - 1] fun addToStack(fileModel: FileModel) { files.add(fileModel) } fun popFromStack() { if (files.isNotEmpty()) files.removeAt(files.size - 1) } fun popFromStackTill(fileModel: FileModel) { files = files.subList(0, files.indexOf(fileModel) + 1) } } |
These are all the methods we need to manipulate our BackStackManager. There is one more convenient thing that we will add here is, a stack change listener. Basically when the stack is modified we want to notify the listener attached to our back stack. Listener functionality is simple and similar to click listener in our recycler adapter.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class BackStackManager { private var files = mutableListOf<FileModel>() var onStackChangeListener: ((List<FileModel>) -> Unit)? = null val top: FileModel get() = files[files.size - 1] fun addToStack(fileModel: FileModel) { files.add(fileModel) onStackChangeListener?.invoke(files) } fun popFromStack() { if (files.isNotEmpty()) files.removeAt(files.size - 1) onStackChangeListener?.invoke(files) } fun popFromStackTill(fileModel: FileModel) { files = files.subList(0, files.indexOf(fileModel) + 1) onStackChangeListener?.invoke(files) } } |
We are done with BackStackManager. Lets wire up the RecyclerView for breadcrumbs!
Never miss a post from TheTechnoCafe
Displaying the Breadcrumbs
If have been following the tutorial properly, your MainActivity.kt should look like this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
class MainActivity : AppCompatActivity(), FilesListFragment.OnItemClickListener { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) window.decorView.systemUiVisibility = window.decorView.systemUiVisibility.or(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) setContentView(R.layout.activity_main) if (savedInstanceState == null) { val filesListFragment = FilesListFragment.build { path = Environment.getExternalStorageDirectory().absolutePath } supportFragmentManager.beginTransaction() .add(R.id.container, filesListFragment) .addToBackStack(Environment.getExternalStorageDirectory().absolutePath) .commit() } } override fun onClick(fileModel: FileModel) { if (fileModel.fileType == FileType.FOLDER) { addFileFragment(fileModel) } else { launchFileIntent(fileModel) } } override fun onLongClick(fileModel: FileModel) { } private fun addFileFragment(fileModel: FileModel) { val filesListFragment = FilesListFragment.build { path = fileModel.path } val fragmentTransaction = supportFragmentManager.beginTransaction() fragmentTransaction.replace(R.id.container, filesListFragment) fragmentTransaction.addToBackStack(fileModel.path) fragmentTransaction.commit() } override fun onBackPressed() { super.onBackPressed() if (supportFragmentManager.backStackEntryCount == 0) { finish() } } } |
Define a variable for BackStackManager and initialise it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
class MainActivity : AppCompatActivity(), FilesListFragment.OnItemClickListener { private val backStackManager = BackStackManager() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) window.decorView.systemUiVisibility = window.decorView.systemUiVisibility.or(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) setContentView(R.layout.activity_main) if (savedInstanceState == null) { val filesListFragment = FilesListFragment.build { path = Environment.getExternalStorageDirectory().absolutePath } supportFragmentManager.beginTransaction() .add(R.id.container, filesListFragment) .addToBackStack(Environment.getExternalStorageDirectory().absolutePath) .commit() } initBackStack() } private fun initBackStack() { backStackManager.onStackChangeListener = { } backStackManager.addToStack(fileModel = FileModel(Environment.getExternalStorageDirectory().absolutePath, FileType.FOLDER, "/", 0.0)) } ... } |
(Hidden most of the code that is being repeated from previous tutorials.)
Now lets initialise the breadcrumb RecyclerView with BreadcrumbRecyclerAdapter.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
class MainActivity : AppCompatActivity(), FilesListFragment.OnItemClickListener { private val backStackManager = BackStackManager() private lateinit var mBreadcrumbRecyclerAdapter: BreadcrumbRecyclerAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) window.decorView.systemUiVisibility = window.decorView.systemUiVisibility.or(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) setContentView(R.layout.activity_main) if (savedInstanceState == null) { val filesListFragment = FilesListFragment.build { path = Environment.getExternalStorageDirectory().absolutePath } supportFragmentManager.beginTransaction() .add(R.id.container, filesListFragment) .addToBackStack(Environment.getExternalStorageDirectory().absolutePath) .commit() } initViews() initBackStack() } private fun initViews() { setSupportActionBar(toolbar) breadcrumbRecyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) mBreadcrumbRecyclerAdapter = BreadcrumbRecyclerAdapter() breadcrumbRecyclerView.adapter = mBreadcrumbRecyclerAdapter mBreadcrumbRecyclerAdapter.onItemClickListener = { supportFragmentManager.popBackStack(it.path, 2); backStackManager.popFromStackTill(it) } } private fun initBackStack() { backStackManager.onStackChangeListener = { } backStackManager.addToStack(fileModel = FileModel(Environment.getExternalStorageDirectory().absolutePath, FileType.FOLDER, "/", 0.0)) } ... } |
On line 34 you can see we are popping the fragments when the user clicks on a particular breadcrumb. First argument is the name given to the path when it is added to the container (which in our case is the path that the fragment represents), the second arguments is a flag which denotes that when the fragment manager starts removing the fragments should it remove the matched fragment as well, in our case we don’t want that so we give any number greater than 1.
Whenever the user clicks on a folder, we need to update the back stack manager, which we will do in the addFileFragment method (already defined in our MainActivity). We also need to pop from back stack when the user presses back button, we do that in onBackPressed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
class MainActivity : AppCompatActivity(), FilesListFragment.OnItemClickListener { private val backStackManager = BackStackManager() private lateinit var mBreadcrumbRecyclerAdapter: BreadcrumbRecyclerAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) window.decorView.systemUiVisibility = window.decorView.systemUiVisibility.or(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) setContentView(R.layout.activity_main) if (savedInstanceState == null) { val filesListFragment = FilesListFragment.build { path = Environment.getExternalStorageDirectory().absolutePath } supportFragmentManager.beginTransaction() .add(R.id.container, filesListFragment) .addToBackStack(Environment.getExternalStorageDirectory().absolutePath) .commit() } initViews() initBackStack() } private fun initViews() { setSupportActionBar(toolbar) breadcrumbRecyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) mBreadcrumbRecyclerAdapter = BreadcrumbRecyclerAdapter() breadcrumbRecyclerView.adapter = mBreadcrumbRecyclerAdapter mBreadcrumbRecyclerAdapter.onItemClickListener = { supportFragmentManager.popBackStack(it.path, 2); backStackManager.popFromStackTill(it) } } private fun initBackStack() { backStackManager.onStackChangeListener = { } backStackManager.addToStack(fileModel = FileModel(Environment.getExternalStorageDirectory().absolutePath, FileType.FOLDER, "/", 0.0)) } private fun addFileFragment(fileModel: FileModel) { val filesListFragment = FilesListFragment.build { path = fileModel.path } backStackManager.addToStack(fileModel) val fragmentTransaction = supportFragmentManager.beginTransaction() fragmentTransaction.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right) fragmentTransaction.replace(R.id.container, filesListFragment) fragmentTransaction.addToBackStack(fileModel.path) fragmentTransaction.commit() } override fun onBackPressed() { super.onBackPressed() backStackManager.popFromStack() if (supportFragmentManager.backStackEntryCount == 0) { finish() } } ... } |
All there is left is to update the BreadcrumbRecyclerAdapter whenever the backstack changes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
class MainActivity : AppCompatActivity(), FilesListFragment.OnItemClickListener { private val backStackManager = BackStackManager() private lateinit var mBreadcrumbRecyclerAdapter: BreadcrumbRecyclerAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) window.decorView.systemUiVisibility = window.decorView.systemUiVisibility.or(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) setContentView(R.layout.activity_main) if (savedInstanceState == null) { val filesListFragment = FilesListFragment.build { path = Environment.getExternalStorageDirectory().absolutePath } supportFragmentManager.beginTransaction() .add(R.id.container, filesListFragment) .addToBackStack(Environment.getExternalStorageDirectory().absolutePath) .commit() } initViews() initBackStack() } private fun initViews() { setSupportActionBar(toolbar) breadcrumbRecyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) mBreadcrumbRecyclerAdapter = BreadcrumbRecyclerAdapter() breadcrumbRecyclerView.adapter = mBreadcrumbRecyclerAdapter mBreadcrumbRecyclerAdapter.onItemClickListener = { supportFragmentManager.popBackStack(it.path, 2); backStackManager.popFromStackTill(it) } } private fun initBackStack() { backStackManager.onStackChangeListener = { updateAdapterData(it) } backStackManager.addToStack(fileModel = FileModel(Environment.getExternalStorageDirectory().absolutePath, FileType.FOLDER, "/", 0.0)) } private fun updateAdapterData(files: List<FileModel>) { mBreadcrumbRecyclerAdapter.updateData(files) if (files.isNotEmpty()) { breadcrumbRecyclerView.smoothScrollToPosition(files.size - 1) } } private fun addFileFragment(fileModel: FileModel) { val filesListFragment = FilesListFragment.build { path = fileModel.path } backStackManager.addToStack(fileModel) val fragmentTransaction = supportFragmentManager.beginTransaction() fragmentTransaction.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right) fragmentTransaction.replace(R.id.container, filesListFragment) fragmentTransaction.addToBackStack(fileModel.path) fragmentTransaction.commit() } override fun onBackPressed() { super.onBackPressed() backStackManager.popFromStack() if (supportFragmentManager.backStackEntryCount == 0) { finish() } } ... } |
On line 50, whenever the back stack changes, we smooth scroll the recycler view to the last element.
Now the application should look and behave as follow:
That is it in this tutorial. In the next tutorial you will implement features to create new folders and files, our BackStackManager will prove helpful in that case as well.
<< Previous Tutorial – Part 3 – Navigating through file system
Next Tutorial – Part 5 – Creating/Deleting Files and Folders >>
3 Comments
Michael Kuhlman · August 14, 2019 at 1:33 pm
Is it okay if we feature your site in our next email newsletter? It’s a perfect fit for a piece we’re doing and I think our audience would find some of the content on your site super useful.
I know you’re probably busy, so just a simple yes or no would suffice.
Many Thanks,
Gurleen Sethi · August 27, 2019 at 5:11 am
Yes, it’s fine go ahead.
spidy · September 14, 2020 at 5:53 pm
I am trying the same in tablayout but breadcrumb is not working properly, it shows directory for second and disappear.
Need suggestion.