I'm using the Jetpack navigation component with a navhost and navgraph. In my categoriesFragment, I have a RecyclerView that displays the current list of categories in the database. It's attached to a LiveData implementation of the SQL that grabs all the categories from the database.
Whenever I add a category to the database from a dialog window, I navigate back to the categoriesFragment while passing data back to it and insert the category into the database from within the categoriesFragment. It shows the updated list of categories in the RecyclerView of the categoriesFragment fine, but the problem is I have to press the back button 2 times to go back to the previous fragment. What could be the issue? Thanks.
Fragment:
package com.example.pomoplay.ui.main
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.NavController
import androidx.navigation.Navigation
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.pomoplay.CategoriesRecyclerAdapter
import com.example.pomoplay.PomoPlayObservablesSingleton
import com.example.pomoplay.R
import kotlinx.android.synthetic.main.fragment_categories.*
class CategoriesFragment : Fragment(), SearchView.OnQueryTextListener{
private var newCategoryCreated: Boolean = false
lateinit var navController: NavController
private var adapter: CategoriesRecyclerAdapter? = null
private val viewModel: CategoriesFragmentViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Log.i("Lifecycle-Fragment", "OnCreateView() called")
return inflater.inflate(R.layout.fragment_categories, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.i("Lifecycle-Fragment", "OnViewCreated() called")
Log.i("Lifecycle-Frag-Bundle", savedInstanceState.toString())
categories_searchview.isIconified = false
categories_searchview.isFocusable = true
categories_searchview.clearFocus()
navController = Navigation.findNavController(view)
observerSetup()
recyclerSetup()
var searchView = categories_searchview
searchView.setOnQueryTextListener(this)
fab_new_category.setOnClickListener {
navController.navigate(R.id.action_categoriesFragment_to_newCategoryDialogFragment)
}
if(!arguments?.isEmpty!! && newCategoryCreated){
var args = CategoriesFragmentArgs.fromBundle(arguments!!)
if(args.fromNewCategoryDialog){
var category = args.category
viewModel.insertCategory(category)
PomoPlayObservablesSingleton.newCategoryCreatedSubject.onNext(false)
}
}
searchView.setOnCloseListener {
viewModel.setLastSearchQuery("")
navController.navigate(R.id.categoriesFragment)
true
}
}
override fun onResume() {
super.onResume()
if(viewModel.getLastSearchQuery() != null && viewModel.getLastSearchQuery().toString() != "")
{
categories_searchview.requestFocusFromTouch()
categories_searchview.setQuery(viewModel.getLastSearchQuery().toString(), true)
}
Log.i("Lifecycle-Fragment", "OnResume() called")
}
override fun onPause() {
super.onPause()
categories_searchview.clearFocus()
Log.i("Lifecycle-Fragment", "OnPause() called")
}
override fun onStop() {
super.onStop()
Log.i("Lifecycle-Fragment", "OnStop() called")
}
override fun onDestroyView() {
super.onDestroyView()
Log.i("Lifecycle-Fragment", "OnDestroyView() called")
//PomoPlayObservablesSingleton.newCategoryCreatedSubject.onNext(false)
}
override fun onDestroy() {
super.onDestroy()
Log.i("Lifecycle-Fragment", "OnDestroy() called")
}
override fun onDetach() {
super.onDetach()
Log.i("Lifecycle-Fragment", "OnDetach() called")
}
private fun observerSetup() {
Log.i("Lifecycle-Fragment", "observerSetup() called")
viewModel.getSearchCategoriesNameResults().observe(this,androidx.lifecycle.Observer { categories ->
Log.i("Lifecycle-Fragment", "getSearchCategoriesNameResults observable value received")
adapter?.setCategoryList(categories)
})
viewModel.getAllCategories()?.observe(this, androidx.lifecycle.Observer { categories ->
Log.i("Lifecycle-Fragment", "getAllCategories observable value received")
if(categories.isNotEmpty()){
adapter?.setCategoryList(categories.sortedBy { category -> category.name?.toLowerCase() })
category_not_found_bubble.visibility = View.GONE
category_not_found_text.visibility = View.GONE
}
else{
category_not_found_bubble.visibility = View.VISIBLE
category_not_found_text.visibility = View.VISIBLE
}
})
PomoPlayObservablesSingleton.newCategoryCreatedSubject.subscribe{comp -> newCategoryCreated = comp }
}
private fun recyclerSetup() {
Log.i("Lifecycle-Fragment", "recyclerSetup() called")
adapter = context?.let { CategoriesRecyclerAdapter(it) }
categories_list?.layoutManager = LinearLayoutManager(context)
categories_list?.adapter = adapter
}
override fun onQueryTextSubmit(query: String?): Boolean {
Log.i("Lifecycle-Fragment", "onQueryTextSubmit() called")
var q = query?.toLowerCase()?.trim()?.replace("\\s+".toRegex(), " ")
setLastSearchQuery(q.toString())
viewModel.searchCategoriesByName(viewModel.getLastSearchQuery().toString())
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
Log.i("Lifecycle-Fragment", "onQueryTextChange() called")
return false
}
private fun setLastSearchQuery(q: String?) {
Log.i("Lifecycle-Fragment", "setLastSearchQuery() called")
viewModel.setLastSearchQuery(q.toString())
}
RecyclerView Adapter:
package com.example.pomoplay
import android.content.Context
import android.view.*
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.widget.PopupMenu
import androidx.navigation.Navigation
import androidx.recyclerview.widget.RecyclerView
import com.example.pomoplay.ui.main.CategoriesFragmentDirections
class CategoriesRecyclerAdapter(private val context: Context) : RecyclerView.Adapter<CategoriesRecyclerAdapter.ViewHolder>(){
private var categoriesList: List<Category> = emptyList()
private val layoutInflater = LayoutInflater.from(context)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemView = layoutInflater.inflate(R.layout.categories_list_item, parent, false)
return ViewHolder(itemView)
}
fun setCategoryList(categories: List<Category>) {
categoriesList = categories
notifyDataSetChanged()
}
override fun getItemCount() = categoriesList.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val category = categoriesList[position]
holder.textCategoryTitle?.text = category?.name
holder.textCategoryDescription?.text = category?.desc
holder.optMenu.setOnClickListener {
val popup = PopupMenu(context, holder.optMenu)
//inflating menu from xml resource
//inflating menu from xml resource
popup.inflate(R.menu.category_menu)
//adding click listener
//adding click listener
popup.setOnMenuItemClickListener(object : MenuItem.OnMenuItemClickListener,
PopupMenu.OnMenuItemClickListener {
override fun onMenuItemClick(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.app_settings -> {
Toast.makeText(context, "it works from recyclerview! :)", Toast.LENGTH_SHORT).show()
true
}
else -> false
}
}
})
//displaying the popup
//displaying the popup
popup.show()
}
holder.textCategoryTitle?.setOnClickListener {
var navController = Navigation.findNavController(it)
var action = CategoriesFragmentDirections.actionCategoriesFragmentToCategoryFragment(category, fromCategoriesFragmentTitle = true)
PomoPlayObservablesSingleton.newCategoryCreatedSubject.onNext(false)
navController.navigate(action)
}
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textCategoryTitle = itemView.findViewById<TextView?>(R.id.categories_list_item_title)
val textCategoryDescription = itemView.findViewById<TextView?>(R.id.categories_list_item_description)
val optMenu: ImageView = itemView.findViewById(R.id.optmenu)
}
RecyclerView Layout:
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/categories_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="10dp"
android:clipToPadding="false"
android:paddingBottom="80dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/categories_searchview"
tools:listitem="#layout/categories_list_item">
</androidx.recyclerview.widget.RecyclerView>
New Category Dialog:
package com.example.pomoplay.ui.main
import android.app.Dialog
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.Spinner
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.navigation.NavController
import androidx.navigation.Navigation
import com.example.pomoplay.Category
import com.example.pomoplay.PomoPlayObservablesSingleton
import com.example.pomoplay.R
class NewCategoryDialogFragment : DialogFragment() {
private lateinit var categoryNameEditText: EditText
private lateinit var categoryDescEditText: EditText
private lateinit var navController: NavController
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
super.onCreateDialog(savedInstanceState)
navController = Navigation.findNavController(parentFragment?.view!!)
var catList = ArrayList<String>()
catList.add("Test Item 1")
catList.add("Test Item 2")
val view =
requireActivity().layoutInflater.inflate(R.layout.fragment_new_category_dialog, null)
categoryNameEditText = view.findViewById(R.id.dialog_new_category_name) as EditText
categoryDescEditText = view.findViewById(R.id.dialog_new_category_desc) as EditText
return activity?.let { it ->
// Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(it)
builder.setTitle("Testing")
.setPositiveButton(
"ok"
) { _, id ->
var cat = Category(categoryNameEditText.text.toString().trim().replace("\\s+".toRegex(), " "), categoryDescEditText.text.toString().trim().replace("\\s+"," "))
var action = NewCategoryDialogFragmentDirections.actionNewCategoryDialogFragmentToCategoriesFragment(cat, fromNewCategoryDialog = true)
PomoPlayObservablesSingleton.newCategoryCreatedSubject.onNext(true)
navController.navigate(action)
}
.setNegativeButton("cancel") { _, id ->
}
.setView(view)
var spinner = view.findViewById<Spinner>(R.id.dialog_new_category_spinner)
var spinnerAdapter =
context?.let {
ArrayAdapter<String>(
it,
android.R.layout.simple_spinner_item,
catList
)
}
spinnerAdapter?.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = spinnerAdapter
val dialog = builder.create()
dialog.setOnShowListener { dialog ->
(dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = categoryNameEditText.text.isNotBlank() && categoryNameEditText.text.isNotEmpty()
}
categoryNameEditText.addTextChangedListener(object : TextWatcher {
override fun onTextChanged(
s: CharSequence, start: Int, before: Int,
count: Int
) {
}
override fun beforeTextChanged(
s: CharSequence, start: Int, count: Int,
after: Int
) {
}
override fun afterTextChanged(s: Editable) { // Check if edittext is empty
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled =
!(categoryNameEditText.text.isBlank() || categoryNameEditText.text.isEmpty())
}
})
dialog
} ?: throw IllegalStateException("Activity cannot be null")
}
}
NavGraph:
<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="#+id/nav_graph_main"
app:startDestination="#id/categoriesFragment">
<fragment
android:id="#+id/clockFragment"
android:name="com.example.pomoplay.ui.main.ClockFragment"
android:label="Pomo Clock"
tools:layout="#layout/fragment_clock" />
<fragment
android:id="#+id/categoryFragment"
android:name="com.example.pomoplay.ui.main.CategoryFragment"
android:label="Category"
tools:layout="#layout/fragment_category">
<action
android:id="#+id/action_categoryFragment_to_clockFragment"
app:destination="#id/clockFragment" />
<argument
android:name="category"
app:argType="com.example.pomoplay.Category"
app:nullable="true"
android:defaultValue="#null" />
<action
android:id="#+id/action_categoryFragment_to_newTaskDialogFragment"
app:destination="#id/newTaskDialogFragment" />
<argument
android:name="pomotask"
app:argType="com.example.pomoplay.PomoTask"
app:nullable="true"
android:defaultValue="#null" />
<argument
android:name="fromNewTaskDialog"
app:argType="boolean"
android:defaultValue="false" />
<argument
android:name="fromCategoriesFragmentTitle"
app:argType="boolean"
android:defaultValue="false" />
</fragment>
<fragment
android:id="#+id/categoriesFragment"
android:name="com.example.pomoplay.ui.main.CategoriesFragment"
android:label="Categories"
tools:layout="#layout/fragment_categories">
<action
android:id="#+id/action_categoriesFragment_to_newCategoryDialogFragment"
app:destination="#id/newCategoryDialogFragment" />
<argument
android:name="category"
app:argType="com.example.pomoplay.Category"
app:nullable="true" />
<action
android:id="#+id/action_categoriesFragment_to_categoryFragment"
app:destination="#id/categoryFragment" />
<argument
android:name="fromNewCategoryDialog"
app:argType="boolean"
android:defaultValue="false" />
</fragment>
<dialog
android:id="#+id/newCategoryDialogFragment"
android:name="com.example.pomoplay.ui.main.NewCategoryDialogFragment"
tools:layout="#layout/fragment_new_category_dialog">
<action
android:id="#+id/action_newCategoryDialogFragment_to_categoriesFragment"
app:destination="#id/categoriesFragment" />
</dialog>
<dialog
android:id="#+id/newTaskDialogFragment"
android:name="com.example.pomoplay.ui.main.NewTaskDialogFragment"
android:label="fragment_new_task_dialog"
tools:layout="#layout/fragment_new_task_dialog" >
<action
android:id="#+id/action_newTaskDialogFragment_to_categoryFragment"
app:destination="#id/categoryFragment" />
</dialog>
</navigation>
You are calling navController.navigate(action) again which will create CategoryFragment again.
To back to previous fragment, you should use navController()?.popBackStack().
I found a solution. If I want to pass data/arguments back to the previous fragment from a dialog fragment and, at the same time, solve the problem of having to press the back button twice, I could use the code in my original question unchanged and just use the Pop Behavior options in the Action attributes of the Action that goes from the newCategoryDialogFragment to the categoriesFragment.
To test it out, I kept the original code in place and in the Pop Behavior section, I set the Pop To attribute to categoriesFragment and checked the Inclusive checkbox.
I had to check the Inclusive checkbox, because when I navigate to the categoresFragment from the newCategoryDialogFragment with navcontroller.navigate(action), it adds the categoriesFragment again to the backstack; the inclusive property removes this duplicate. Here's a screenshot of the Pop Behavior section:
Related
When trying to write setHasOptionsMenu(true) in onCreate and override fun onCreateOptionsMenu as usual, Android Studio crosses out these functions saying that they are deprecated.
I looked at what they suggest
https://developer.android.com/jetpack/androidx/releases/activity?authuser=5#1.4.0-alpha01
and it turns out that they are asking to insert some new functions in Activity (MainActivity.kt) and some in Fragment (DogListFragment.kt). But in my app, all menu customization was done only in Fragment, so Activity can't do that. Activity simply doesn't have access to the RecyclerView, which is in the layout (fragment_god_list.xml) that belongs to Fragment. Activity only has androidx.fragment.app.FragmentContainerView in its activity_main.xml
Does anyone know how this can be done in Fragment without having to do anything with the menus in Activity?
GitHub project: https://github.com/theMagusDev/DogglersApp
MainActivity.kt:
package com.example.dogglers
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupActionBarWithNavController
import com.example.dogglers.databinding.ActivityMainBinding
private lateinit var navController: NavController
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Setup view binding
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Setup navController
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
setupActionBarWithNavController(navController)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
}
DogListFragment.kt:
package com.example.dogglers
import android.os.Bundle
import android.view.*
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.dogglers.adapter.DogCardAdapter
import com.example.dogglers.const.Layout
import com.example.dogglers.databinding.FragmentDogListBinding
class DogListFragment : Fragment() {
private var _binding: FragmentDogListBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
private lateinit var recyclerView: RecyclerView
private var layoutType = Layout.VERTICAL
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentDogListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
recyclerView = binding.verticalRecyclerView
setUpAdapter()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.layout_manu, menu)
val layoutButton = menu.findItem(R.id.action_switch_layout)
// Calls code to set the icon
setIcon(layoutButton)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_switch_layout -> {
layoutType = when (layoutType) {
Layout.VERTICAL -> Layout.HORIZONTAL
Layout.HORIZONTAL -> Layout.GRID
else -> Layout.VERTICAL
}
setUpAdapter()
return true
}
// Otherwise, do nothing and use the core event handling
// when clauses require that all possible paths be accounted for explicitly,
// for instance both the true and false cases if the value is a Boolean,
// or an else to catch all unhandled cases.
else -> return super.onOptionsItemSelected(item)
}
}
fun setUpAdapter() {
recyclerView.adapter = when(layoutType){
Layout.VERTICAL -> {
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
DogCardAdapter(
context,
Layout.VERTICAL
)
}
Layout.HORIZONTAL -> {
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
DogCardAdapter(
context,
Layout.HORIZONTAL
)
}
else -> {
recyclerView.layoutManager = GridLayoutManager(context, 2, RecyclerView.VERTICAL, false)
DogCardAdapter(
context,
Layout.GRID
)
}
}
}
private fun setIcon(menuItem: MenuItem?) {
if (menuItem == null)
return
menuItem.icon = when(layoutType) {
Layout.VERTICAL -> ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_vertical_layout)
Layout.HORIZONTAL -> ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_horizontal_layout)
else -> ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_grid_layout)
}
}
}
ActivityMain.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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.fragment.app.FragmentContainerView
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph"/>
</FrameLayout>
FragmentDogList:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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.recyclerview.widget.RecyclerView
android:id="#+id/vertical_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="#layout/vertical_list_item"/>
</FrameLayout>
When you call setUpActionBarWithNavController() method , you are setting up toolbar inside the activity. Your Fragment is inside this activity. Your fragment has this actionBar too. To use Menu provider inside fragment, you need to call below method inside of onViewCreated() method of fragment.
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
Also, You need to make your fragment implement MenuProvider Interface
class DogListFragment : Fragment(),MenuProvider {...
IDE will ask you to implement its provider method i.e onCreateMenu and onMenuItemSelected
Inside OnCreateMenu, use menu Inflator to inflate menu layout
example:-
menuInflater.inflate(R.menu.search_menu,menu)
I would like to navigate between fragments using a textView inside a recyclerview.
Currently I am successfully navigating between fragments using the item, but I would like to go further and navigate using the individual textViews within an item.
The recyclerview is inflated from a local room database.
Below is my adapter
package com.benb.inventory
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.benb.inventory.data.Item
import com.benb.inventory.data.getFormattedPrice
import com.benb.inventory.databinding.ItemListItemBinding
class ItemListAdapter(private val onItemClicked: (Item) -> Unit) :
ListAdapter<Item, ItemListAdapter.ItemViewHolder>(DiffCallback) {
class ItemViewHolder(internal var binding: ItemListItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Item) {
binding.apply {
itemName.text = item.itemName
itemPrice.text = item.getFormattedPrice()
itemShop.text = item.shop.toString()
itemShop.setOnClickListener{
val shopName = itemShop.text.toString()
Toast.makeText(root.context, "Clicked: ${item.shop}", Toast.LENGTH_SHORT ).show()
}
}
}
}
companion object {
private val DiffCallback = object : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem.itemName == newItem.itemName
}
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ItemListAdapter.ItemViewHolder {
return ItemViewHolder(ItemListItemBinding.inflate(LayoutInflater.from(parent.context)))
}
override fun onBindViewHolder(holder: ItemListAdapter.ItemViewHolder, position: Int) {
val current = getItem(position)
holder.itemView.setOnClickListener {
onItemClicked(current)
}
holder.binding.itemShop.setOnClickListener {
onItemClicked(current)
val shopName = current.shop
fun showShopList(shopName: String) {
val shopName = holder.binding.itemShop.text.toString()
val action = ItemListFragmentDirections.actionItemListFragmentToShopItemFragment(shopName)
}
}
holder.bind(current)
}
}
This is the fragment that contains the list
package com.benb.inventory
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.benb.inventory.databinding.ItemListFragmentBinding
import kotlinx.coroutines.InternalCoroutinesApi
#InternalCoroutinesApi
class ItemListFragment : Fragment() {
#InternalCoroutinesApi
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database.itemDao()
)
}
private var _binding: ItemListFragmentBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = ItemListFragmentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = ItemListAdapter {
val action = ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
this.findNavController().navigate(action)
}
binding.recyclerView.adapter = adapter
viewModel.allItems.observe(this.viewLifecycleOwner) {items ->
items.let {
adapter.submitList(it)
}
}
binding.itemShop.setOnClickListener{
val shop = binding.itemShop.text.toString()
val action = ItemListFragmentDirections.actionItemListFragmentToShopItemFragment(shop)
viewModel.retrieveShopItems(shop)
this.findNavController().navigate(action)
}
binding.recyclerView.layoutManager = LinearLayoutManager(this.context)
binding.floatingActionButton.setOnClickListener {
val action = ItemListFragmentDirections.actionItemListFragmentToAddItemFragment(
getString(R.string.add_fragment_title)
)
this.findNavController().navigate(action)
}
}
}
For clarity this is what the inflated recyclerview looks like, it can contain many items, I have only added one.
At the moment if I click anywhere on the item it takes you to a screen with more detail about the fragment.
[A screenshot of the recycler view][1]
I would like to be able to navigate to a specific fragment depending on the textView clicked.
For example one fragment that only contains other products from the same shop.
As you may notice, I have been able to add an onClickListener in the ViewHolder class, it creates a Toast.
I have not had success in using the same onClickListener to navigate.
Thank you very much.
P.S. Any other advice is welcome, particularly if anyone knows what the #internalcoroutinesAPI thing is about please tell me!
[1]: https://i.stack.imgur.com/k6Td3.png
Currently, I am trying to implement RecyclerView inside of Fragment but I cannot find a way to display it.
Here is my MainActivity:
package com.example
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import com.example.projectdrivemark.R
import com.example.projectdrivemark.databinding.ActivityMainBinding
import com.example.recyclerView.MockDatabase.Companion.createMockData
import com.example.recyclerView.PostAdapter
import com.example.recyclerView.RecyclerViewFragment
import com.example.tempConverter.TempConverterFragment
import com.example.uploaderView.UploaderFragment
class MainActivity : AppCompatActivity(), PostAdapter.OnPostClickListener {
private lateinit var binding: ActivityMainBinding
val dummyList = createMockData()
val adapter = PostAdapter(dummyList, this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
title = "First Kotlin App"
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val recyclerView = RecyclerViewFragment()
val tempConverterView = TempConverterFragment()
// recyclerView.layoutManager = LinearLayoutManager(this)
val uploaderView = UploaderFragment(this)
setFragmentView(recyclerView)
binding.bottomNavBar.setOnNavigationItemSelectedListener {
when(it.itemId){
R.id.listView -> setFragmentView(recyclerView)
R.id.tempConverterView -> setFragmentView(tempConverterView)
R.id.videoUploaderView -> setFragmentView(uploaderView)
}
true
}
}
private fun setFragmentView(fragment: Fragment){
supportFragmentManager.beginTransaction().apply {
replace(R.id.main_fragment_view, fragment)
//Will return to previous page when tap "Back Button" on the phone
addToBackStack(null)
commit()
}
}
override fun onEditPost(position: Int){
val clickedPost = dummyList[position]
clickedPost.title = "Updated title"
clickedPost.body = "Updated body"
adapter.notifyItemChanged(position)
}
override fun onDeletePost(position: Int) {
dummyList.removeAt(position)
adapter.notifyItemRemoved(position)
}
fun celsiusFunction(view: View) {
val tempConverter = TempConverterFragment()
tempConverter.celsiusFunction(view)
}
fun farenheitFunction(view: View){
val fahrenheitConverter = TempConverterFragment()
fahrenheitConverter.farenheitFunction(view)
}
}
Here is my RecyclerFragment:
package com.example.recyclerView
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.MainActivity
import com.example.projectdrivemark.R
class RecyclerViewFragment: Fragment() {
var adapter: RecyclerView.Adapter<PostAdapter.PostViewHolder>? = null
var layoutManager: RecyclerView.LayoutManager? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreate(savedInstanceState)
val binding = inflater.inflate(R.layout.fragment_list, container, false)
return binding
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.apply{
layoutManager = LinearLayoutManager(activity)
adapter = PostAdapter(dummyData = ArrayList<Post>(), MainActivity())
}
}
}
Here is my PostAdapter code:
package com.example.recyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.MainActivity
import com.example.projectdrivemark.R
class PostAdapter(val dummyData: ArrayList<Post>, val myListener: MainActivity) : RecyclerView.Adapter<PostAdapter.PostViewHolder>() {
inner class PostViewHolder(postView: View) : RecyclerView.ViewHolder(postView), View.OnClickListener{
val iconImage: ImageView = postView.findViewById(R.id.icon_image_view)
val title: TextView = postView.findViewById(R.id.title)
val body: TextView = postView.findViewById(R.id.body)
val deleteIcon: ImageView = postView.findViewById(R.id.delete_post_image)
val editIcon: ImageView = postView.findViewById(R.id.edit_post_image)
init {
deleteIcon.setOnClickListener(this)
editIcon.setOnClickListener(this)
}
override fun onClick(v: View?){
val position = adapterPosition
if(v?.id == editIcon.id){
myListener.onEditPost(position)
}else{
myListener.onDeletePost(position)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder {
val postView = LayoutInflater.from(parent.context).inflate(R.layout.post, parent, false)
return PostViewHolder(postView)
}
override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
val currentPost = dummyData[position]
holder.iconImage.setImageResource(currentPost.image)
holder.title.text = currentPost.title
holder.body.text = currentPost.body
}
override fun getItemCount(): Int {
return dummyData.size
}
interface OnPostClickListener{
fun onEditPost(position: Int)
fun onDeletePost(position: Int)
}
}
If anyone saw something I miss, please do tell me because I am stuck at how to display the RecyclerView. Any help would be appreciate.
Edit:
Here is my MockDatabase:
package com.example.recyclerView
import com.example.projectdrivemark.R
class MockDatabase {
companion object{
fun createMockData(): ArrayList<Post>{
val list = ArrayList<Post>()
for(i in 0 until 20){
val imageToSelect = when (i % 3){
0 -> R.drawable.ic_baseline_account_balance
1 -> R.drawable.ic_baseline_account_circle
2 ->R.drawable.ic_baseline_ac_unit
else -> R.drawable.ic_baseline_access_alarms
}
list.add(
Post(
imageToSelect,
title = "Title post of $i",
body = "Title post of $i"
)
)
}
return list
}
}
}
Here is my Layout_list xml file:
<?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">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recycleViewMain"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
tools:listitem="#layout/recycler_view_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
You need to cast view to RecyclerView to access RecyclerView properties inside apply, also you cannot create instances of Activity by yourself,
MainActivity() is wrong and won't work, instead use requireActivity() which will provide context of Activity to which Fragment is attached to
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewbyId<RecyclerView>(R.id.recycleViewMain).apply{
layoutManager = LinearLayoutManager(requireActivity())
adapter = PostAdapter(dummyData = ArrayList<Post>(), requireActivity() as OnPostClickListener)
}
}
Also, as #Necronet mentioned, add some mock data to see if it is actually rendering.
UPDATE
Instead of passing MainActivity to Adapter, pass OnPostClickListener
adapter = PostAdapter(dummyData = ArrayList<Post>(), requireActivity() as OnPostClickListener)
Adapter
class PostAdapter(val dummyData: ArrayList<Post>, val myListener: OnPostClickListener) : RecyclerView.Adapter<PostAdapter.PostViewHolder>(){
// you can use myListener to call methods
}
It's simple you are passing empty dummyData try using createMockData() instead. Also as a rule of thumb in Android, never ever instatiate an Activity yourself, if you need the reference from within a fragment you can use getActivity() method. So this:
adapter = PostAdapter(dummyData = ArrayList<Post>(), MainActivity())
Should be:
adapter = PostAdapter(dummyData = createMockData(), getActivity() as MainActivity)
Whenever I use the Notification Shade to switch between the device dark and light themes, (my app when running) always seems to crash for some reason. My app's minimum API is 29 (Android 10). The logcat points to a line of code where the reason for the error isn't obvious. (import android.view.*). How can I prevent this from happening again?
java.lang.NullPointerException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkNotNullParameter, parameter view
Activity class
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) supportFragmentManager.beginTransaction()
.replace(R.id.detail_container, MainFragment())
.commitNow()
}
}
Main fragment class
package com.example.vp2
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.text.TextUtils
import android.view.*
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.ActionBar
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.app.NavUtils
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import java.util.*
class MainFragment : androidx.fragment.app.Fragment() {
private lateinit var mySpinnerItems: Array<String>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val v = inflater.inflate(R.layout.fragment_main, container, false)
super.onCreate(savedInstanceState)
return v
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
val mSpinner = requireView().findViewById<Spinner>(R.id.mSpinner)
val mViewPager2 = requireView().findViewById<ViewPager2>(R.id.mViewPager2)
// Spinner items array
mySpinnerItems = arrayOf(
"Item 1",
"Item 2",
"Item 3",
)
val arrayAdapter = ArrayAdapter(requireView().context, android.R.layout.simple_spinner_item, mySpinnerItems)
arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_item)
mSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
when {
mySpinnerItems[position] == "Item 1" -> {
mViewPager2.setCurrentItem(0, false)
}
mySpinnerItems[position] == "Item 2" -> {
mViewPager2.setCurrentItem(1, false)
}
else -> {
mViewPager2.setCurrentItem(2, false)
}
}
}
override fun onNothingSelected(parent: AdapterView<*>) {
mViewPager2.setCurrentItem(0, false)
}
}
mSpinner.adapter = arrayAdapter
mViewPager2.adapter = MySpinnerFragmentAdapter(this)
mViewPager2.orientation = ViewPager2.ORIENTATION_HORIZONTAL
mViewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
when (position) {
0 -> {
mSpinner.setSelection(0)
}
1 -> {
mSpinner.setSelection(1)
}
else -> {
mSpinner.setSelection(2)
}
}
super.onPageSelected(position)
}
})
super.onActivityCreated(savedInstanceState)
}
private class MySpinnerFragmentAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
private val intItems = 3
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> Fragment1()
1 -> Fragment2()
else -> Fragment3()
}
}
override fun getItemCount(): Int {
return intItems
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
menu.clear()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == android.R.id.home) {
val intent = activity?.let { NavUtils.getParentActivityIntent(it) }
when {
intent != null -> {
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
NavUtils.navigateUpTo(requireActivity(), intent)
}
}
true
} else super.onOptionsItemSelected(item)
}
}
Main activity layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/detail_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
</LinearLayout>
Main fragment layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/detail_container">
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/mViewPager2"
android:layout_height="0dp"
android:layout_width="match_parent"
android:layout_weight="1" />
<!-- divider (start)-->
<View
android:id="#+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="5dp"
android:background="?android:attr/textColorSecondary" />
<!-- divider (end)-->
<Spinner
android:id="#+id/mSpinner"
style="#style/Widget.AppCompat.Spinner.Underlined"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:spinnerMode="dropdown"/>
</LinearLayout>
UPDATE
cactustictacs' suggestion
mSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
when {
mySpinnerItems[position] == "Item 1" -> {
mViewPager2.setCurrentItem(0, false)
}
mySpinnerItems[position] == "Item 2" -> {
mViewPager2.setCurrentItem(1, false)
}
else -> {
mViewPager2.setCurrentItem(2, false)
}
}
}
override fun onNothingSelected(parent: AdapterView<*>) {
// Code to perform some action when nothing is selected
mViewPager2.setCurrentItem(0, false)
}
}
The stacktrace (please post the actual text in future not a screenshot!) is saying some parameter called view is null when its type is specified as non-null (View instead of View?). And that's being caused by AdapterView.onItemSelected running (so it's the view parameter in that method), which is declared in onActivityCreated
Basically that onItemSelected method needs to have nullable types for the first two parameters, AdapterView<*>? and View?.
That's what you get if you let the IDE auto-implement the methods (with ctrl+I) - the docs have that View! type which means because it's coming from Java, it could be null, might not, don't know - so the safe default is View?. When you specify a non-null type (like View) Kotlin does a null check to make sure - that's what that Intrinsics.checkNotNullParameter call is. You got a null, so it threw an exception!
So yeah, make them nullable, then null-check them before you access them. Also make sure appcompat is up to date (at least 1.2.0) because they had an issue where Activitys weren't being recreated when you used setDefaultNightMode
I have a Fragment MyFragment currently which has a Spinner my_spinner. For testing my app, I originally populated the contents of my_spinner manually by observing the property myLiveDataList in the AndroidViewModel MyViewModel as below:
my_fragment.xml
<?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"
tools:context=".ui.fragments.MyFragment">
<Spinner
android:id="#+id/my_spinner"
android:layout_width="match_parent"
android:layout_height="100dp" />
</FrameLayout>
MyFragment.kt
import androidx.lifecycle.ViewModelProviders
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.lifecycle.Observer
import com.example.app.R
import com.example.app.data.room.entities.MyEntity
import com.example.app.ui.viewmodels.MyViewModel
import kotlinx.android.synthetic.main.my_fragment.*
class MyFragment : Fragment() {
companion object {
fun newInstance() = MyFragment()
}
private lateinit var viewModel: MyViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.my_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
val myAdapter = ArrayAdapter<MyEntity>(this.context!!, android.R.layout.simple_spinner_item)
// This is where I populate my_spinner
viewModel.myLiveDataList.observe(this, Observer<List<MyEntity>> { data ->
data?.forEach {
myAdapter.add(it)
}
})
my_spinner.adapter = myAdapter
}
}
MyViewModel.kt
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.toLiveData
import com.example.app.data.repositories.MyRepository
import com.example.app.data.room.entities.MyEntity
class MyViewModel(application: Application) : AndroidViewModel(application) {
private val myRepository = MyRepository(application)
val myLiveDataList: LiveData<List<MyEntity>>
get() = myRepository.getAllData().toLiveData()
}
This fills my_spinner successfully when I navigate to MyFragment:
Since it populates as expected, I went ahead to make the following changes to my_fragment.xml:
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="viewmodel"
type="com.example.app.ui.viewmodels.MyViewModel" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.fragments.MyFragment">
<Spinner
android:id="#+id/my_spinner"
android:layout_width="match_parent"
android:layout_height="100dp"
app:entries="#{viewmodel.myLiveDataList}"/>
</FrameLayout>
</layout>
I've added in a Binding Adapter file BindingAdapterUtil (following code was copied from this article):
import android.R
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import androidx.databinding.BindingAdapter
import androidx.databinding.InverseBindingAdapter
import androidx.databinding.InverseBindingListener
import com.example.app.ui.adapter.SpinnerExtensions.getSpinnerValue
import com.example.app.ui.adapter.SpinnerExtensions.setSpinnerEntries
import com.example.app.ui.adapter.SpinnerExtensions.setSpinnerInverseBindingListener
import com.example.app.ui.adapter.SpinnerExtensions.setSpinnerItemSelectedListener
import com.example.app.ui.adapter.SpinnerExtensions.setSpinnerValue
#BindingAdapter("entries")
fun Spinner.setEntries(entries: List<Any>?) {
setSpinnerEntries(entries)
}
#BindingAdapter("onItemSelected")
fun Spinner.setItemSelectedListener(itemSelectedListener: SpinnerExtensions.ItemSelectedListener?) {
setSpinnerItemSelectedListener(itemSelectedListener)
}
#BindingAdapter("newValue")
fun Spinner.setNewValue(newValue: Any?) {
setSpinnerValue(newValue)
}
#BindingAdapter("selectedValue")
fun Spinner.setSelectedValue(selectedValue: Any?) {
setSpinnerValue(selectedValue)
}
#BindingAdapter("selectedValueAttrChanged")
fun Spinner.setInverseBindingListener(inverseBindingListener: InverseBindingListener?) {
setSpinnerInverseBindingListener(inverseBindingListener)
}
#InverseBindingAdapter(attribute = "selectedValue", event = "selectedValueAttrChanged")
fun Spinner.getSelectedValue(): Any? {
return getSpinnerValue()
}
object SpinnerExtensions {
fun Spinner.setSpinnerEntries(entries: List<Any>?) {
if (entries != null) {
val arrayAdapter = ArrayAdapter(context, R.layout.simple_spinner_item, entries)
arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
adapter = arrayAdapter
}
}
fun Spinner.setSpinnerItemSelectedListener(listener: ItemSelectedListener?) {
if (listener == null) {
onItemSelectedListener = null
} else {
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
if (tag != position) {
listener.onItemSelected(parent.getItemAtPosition(position))
}
}
override fun onNothingSelected(parent: AdapterView<*>) {}
}
}
}
fun Spinner.setSpinnerInverseBindingListener(listener: InverseBindingListener?) {
if (listener == null) {
onItemSelectedListener = null
} else {
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
if (tag != position) {
listener.onChange()
}
}
override fun onNothingSelected(parent: AdapterView<*>) {}
}
}
}
fun Spinner.setSpinnerValue(value: Any?) {
if (adapter != null ) {
val position = (adapter as ArrayAdapter<Any>).getPosition(value)
setSelection(position, false)
tag = position
}
}
fun Spinner.getSpinnerValue(): Any? {
return selectedItem
}
interface ItemSelectedListener {
fun onItemSelected(item: Any)
}
}
And I've modified the onActivityCreated in MyFragment like so:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
DataBindingUtil.setContentView<MyFragmentBinding>(
this.activity!!, R.layout.my_fragment
).apply {
this.setLifecycleOwner(this#MyFragment)
this.viewmodel = viewModel
}
}
The result of this is that my_spinner is no longer populating with the contents of MyViewModel.myLiveDataList. To try to ascertain if the property was at fault, I created a new property in MyViewModel like so:
val myList: List<String>?
get() = listOf("First", "Second", "Third")
And I have bound this property to my_spinner just like MyViewModel.myLiveDataList above with success this time.
The function in MyRepository.getAllData() (which myLiveDataList returns) returns a Flowable<List<MyEntity>> (RxJava), which calls a Room DAO to get the data. My assumption here is that myLiveDataList doesn't have anything to serve when it tries to bind the values for the first time, and never tries again.
Am I missing something when trying to bind a LiveData datasource to a Spinner?
After reading this answer, I've modified my_fragment.xml to the following:
...
<data>
<import type="java.util.List" />
<import type="com.example.app.data.room.entities.MyEntity" />
<import type="androidx.lifecycle.LiveData" />
<variable name="viewmodel"
type="com.example.app.ui.viewmodels.MyViewModel" />
<variable name="myTestList"
type="LiveData<List<MyEntity>>" />
</data>
...
<Spinner
android:id="#+id/my_spinner"
android:layout_width="match_parent"
android:layout_height="100dp"
app:entries="#{myTestList}"/>
...
I've also removed the contents of MyFragment.onActivityCreated and modified MyFragment.onCreateView as followed:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
val binding = MyFragmentBinding.inflate(inflater, container, false)
binding.setLifecycleOwner(this)
binding.viewmodel = viewModel
binding.myTestList = viewModel.myLiveDataList
return binding.root
}
Not a perfect solution, and I still don't know why my original bout at this problem didn't yield the desired results, but it will do. If there is a better way of binding a Spinner to LiveData in this fashion, please let me know.