I have 2 menu items (filter button and search button) that allow to filter or search within a list. When a list item is selected, it is given to the view throught the viewModel's LiveData called listItemSelected, when this happen, we move from the ListFragment to the DetailFragment and hide the 2 buttons from the menu, as they aren't relevent here.
My problem is on configuration change, for example on screen rotation, the old activity is destroyed, and the new Activity.onCreate() function is called before onCreateOptionsMenu(), so I don't know how can I set back the menu state properly.
Simplified code sample below, right now it just crash on filterMenuItem.isVisible = it == null because filterMenuItem is not initialized at this point.
class MyActivity : AppCompatActivity() {
private lateinit var viewModel: MyViewModel
private lateinit var filterMenuItem: MenuItem
private lateinit var searchMenuItem: MenuItem
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = getViewModel { injector.myViewModel }
viewModel.listItemSelected.observe(this, Observer {
filterMenuItem.isVisible = it == null
searchMenuItem.isVisible = it == null
})
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.main_menu, menu)
menu!!.apply {
filterMenuItem = findItem(R.id.main_menu_filter)
searchMenuItem = findItem(R.id.main_menu_search)
}
return true
}
How about observing data after you initialized the MenuItem's:
class MyActivity : AppCompatActivity() {
private lateinit var viewModel: MyViewModel
private lateinit var filterMenuItem: MenuItem
private lateinit var searchMenuItem: MenuItem
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = getViewModel { injector.myViewModel }
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.main_menu, menu)
menu!!.apply {
filterMenuItem = findItem(R.id.main_menu_filter)
searchMenuItem = findItem(R.id.main_menu_search)
}
// observe after the menu items are initialized
viewModel.listItemSelected.observe(this, Observer {
filterMenuItem.isVisible = it == null
searchMenuItem.isVisible = it == null
})
return true
}
}
Related
I have an activity that has a SearchView that I use to enter a query, my app then uses to query to access an API. My activity further contains a fragment, and within this fragment I have my observer.
Further I have my ViewModel, which makes the API call when given a query. However, my observer is never notified about the update, and thus my view never updates. Unless I call it directly from my ViewModel upon initiation. I'll show it specifically here:
ViewModel
class SearchViewModel : ViewModel() {
val booksResponse = MutableLiveData<MutableList<BookResponse>>()
val loading = MutableLiveData<Boolean>()
val error = MutableLiveData<String>()
init {
getBooks("How to talk to a widower")
}
fun getBooks(bookTitle: String) {
GoogleBooksService.api.getBooks(bookTitle).enqueue(object: Callback<ResponseWrapper<BookResponse>> {
override fun onFailure(call: Call<ResponseWrapper<BookResponse>>, t: Throwable) {
onError(t.localizedMessage)
}
override fun onResponse(
call: Call<ResponseWrapper<BookResponse>>,
response: Response<ResponseWrapper<BookResponse>>
) {
if (response.isSuccessful){
val books = response.body()
Log.w("2.0 getFeed > ", Gson().toJson(response.body()));
books?.let {
// booksList.add(books.items)
booksResponse.value = books.items
loading.value = false
error.value = null
Log.i("Content of livedata", booksResponse.getValue().toString())
}
}
}
})
}
private fun onError(message: String) {
error.value = message
loading.value = false
}
}
Query Submit/ Activity
class NavigationActivity : AppCompatActivity(), SearchView.OnQueryTextListener, BooksListFragment.TouchActionDelegate {
lateinit var searchView: SearchView
lateinit var viewModel: SearchViewModel
private val mOnNavigationItemSelectedListener =
BottomNavigationView.OnNavigationItemSelectedListener { menuItem ->
when (menuItem.itemId) {R.id.navigation_search -> {
navigationView.getMenu().setGroupCheckable(0, true, true);
replaceFragment(SearchListFragment.newInstance())
return#OnNavigationItemSelectedListener true
}
R.id.navigation_books -> {
navigationView.getMenu().setGroupCheckable(0, true, true);
replaceFragment(BooksListFragment.newInstance())
return#OnNavigationItemSelectedListener true
}
}
false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
replaceFragment(SearchListFragment.newInstance())
navigationView.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
//Set action bar color
val actionBar: ActionBar?
actionBar = supportActionBar
val colorDrawable = ColorDrawable(Color.parseColor("#FFDAEBE9"))
// actionBar!!.setBackgroundDrawable(colorDrawable)
// actionBar.setTitle(("Bobs Books"))
setSupportActionBar(findViewById(R.id.my_toolbar))
viewModel = ViewModelProvider(this).get(SearchViewModel::class.java)
}
override fun onBackPressed() {
super.onBackPressed()
navigationView.getMenu().setGroupCheckable(0, true, true);
}
private fun replaceFragment(fragment: Fragment){
supportFragmentManager
.beginTransaction()
.replace(R.id.fragmentHolder, fragment)
.commit()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.book_search_menu, menu)
val searchItem = menu.findItem(R.id.action_search)
searchView = searchItem.actionView as SearchView
searchView.setOnQueryTextListener(this)
searchView.queryHint = "Search for book"
/*searchView.onActionViewExpanded()
searchView.clearFocus()*/
// searchView.setIconifiedByDefault(false)
return true
}
override fun onQueryTextSubmit(query: String): Boolean {
//replaces fragment if in BooksListFragment when searching
replaceFragment(SearchListFragment.newInstance())
val toast = Toast.makeText(
applicationContext,
query,
Toast.LENGTH_SHORT
)
toast.show()
searchView.setQuery("",false)
searchView.queryHint = "Search for book"
// viewModel.onAddBook(Book(title = query!!, rating = 5, pages = 329))
Log.i("Query fra text field", query)
// viewModel.getBooks(query)
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
return false
}
override fun launchBookFragment(bookId: Book) {
supportFragmentManager
.beginTransaction()
.replace(R.id.fragmentHolder, com.example.bobsbooks.create.BookFragment.newInstance(bookId.uid))
.addToBackStack(null)
.commit()
navigationView.getMenu().setGroupCheckable(0, false, true);
}
}
Fragment
class SearchListFragment : Fragment() {
lateinit var viewModel: SearchViewModel
lateinit var contentListView: SearchListView
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_search_list, container, false).apply {
contentListView = this as SearchListView
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
bindViewModel()
setContentView()
}
private fun setContentView(){
contentListView.initView()
}
private fun bindViewModel(){
Log.i("ViewmodelCalled", "BindViewModel has been called")
viewModel = ViewModelProvider(this).get(SearchViewModel::class.java)
viewModel.booksResponse.observe(viewLifecycleOwner, Observer {list ->
list?.let {
Log.i("Observer gets called", "Updatelistgetscalled")
contentListView.updateList(list)
}
} )
viewModel.error.observe(viewLifecycleOwner, Observer { errorMsg ->
})
viewModel.loading.observe(viewLifecycleOwner, Observer { isLoading ->
})
}
companion object {
fun newInstance(): SearchListFragment {
return SearchListFragment()
}
}
When I put the getBooks call into my Viewmodel Init, it will do everything correctly. It gets the bookresponse through the API, adds it to my LiveData and notifies my adapter.
However, if I instead delete that and call it through my Querysubmit in my Activity, it will, according to my logs, get the data and put it into my booksReponse:LiveData, but thats all it does. The observer is never notifed of this change, and thus the adapter never knows that it has new data to populate its views.
I feel like I've tried everything, I even have basically the same code working in another app, where it runs entirely in an activity instead of making the query in an activity, and rest is called in my fragment. My best guess is this has an impact, but I cant figure out how.
As per your explanation
However, if I instead delete that and call it through my Querysubmit in my Activity, it will, according to my logs, get the data and put it into my booksReponse:LiveData, but thats all it does. The observer is never notifed of this change, and thus the adapter never knows that it has new data to populate its views.
the problem is you are initializing SearchViewModel in both activity & fragment, so fragment doesn't have the same instance of SearchViewModel instead you should use shared viewmodel in fragment like :
viewModel = ViewModelProvider(requireActivity()).get(SearchViewModel::class.java)
I have a fragment for logging into my app, and that fragment is displayed via an activity. If the user logs in successfully, then I need to add a menu to the parent activity. How to do that??
Menus are inflated during Activity creation. So, in your case, you can only play with it visibility.
This is not a working code but just that you can get the idea:
class MainActivity : AppCompatActivity(), MFragment.Listener {
...
var menu: Menu? = null
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
this.menu = menu
menuInflater.inflate(R.menu.create_order_menu, menu)
menu?.findItem(R.id.m_item)?.apply {
isVisible = false
}
return true
}
override fun onLoginSucceed() {
menu?.findItem(R.id.m_item)?.apply {
isVisible = true
}
}
}
MFragment
class MFragment : Fragment() {
private var listener: Listener? = null
interface Listener {
fun onLoginSucceed()
}
override fun onAttach(context: Context) {
super.onAttach(context)
listener = context as Listener
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
listener?.onLoginSucceed()
}
companion object {
#JvmStatic fun newInstance() = MFragment()
}
}
I have an activity which has a RecyclerView I'd like to filter using a SearchView I set up.
class ExerciseCatalogueActivity : AppCompatActivity(), ExerciseClickedListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_exercise_catalogue_activity)
val toolbar = toolbar_catalogue
setSupportActionBar(toolbar)
val exercisesCatalogueList : MutableList<Exercise>
val exerciseCatalogueFullList: MutableList<Exercise>
val exerciseFileString: String = resources.openRawResource(R.raw.exercises1).bufferedReader().use { it.readText() }
exercisesCatalogueList = (Gson().fromJson(exerciseFileString, Array<Exercise>::class.java)).toMutableList()
exerciseCatalogueFullList = ArrayList<Exercise>(exercisesCatalogueList)
rv_catalogue.apply {
rv_catalogue.layoutManager = LinearLayoutManager(context, LinearLayout.VERTICAL, false)
adapter = ExerciseCatalogueRecyclerViewAdapter(exercisesCatalogueList,exerciseCatalogueFullList, this#ExerciseCatalogueActivity)
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.toolbar_menu, menu)
val searchItem = menu!!.findItem(R.id.action_search_bar)
val searchView = searchItem.actionView as SearchView
searchView.imeOptions = EditorInfo.IME_ACTION_DONE
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return false
}
override fun onQueryTextChange(newText: String): Boolean {
adapter.filter.filter(newText)
return false
}
})
return true
}
}
However adapter.filter.filter(newText) returns as unresolved and I cannot filter my recyclerView. What changes need to be made?
You haven't saved a reference to your adapter.
You use .apply on the RecyclerView called rv_catalogue, so that works inside the scope.
In onCreateOptionsMenu, there is no adapter to reference.
Either save a reference to the adapter in your Activity, or use rv_catalogue.adapter to get a reference to the adapter.
My code as is follows:
class MySitesActivity : AppCompatActivity() {
val REQUEST_CODE = 3
private val TAG = "MySitesActivity"
lateinit var gridView: GridView
lateinit var siteAdapter:BaseAdapter
lateinit var sites:ArrayList<Site>
lateinit var actionBarObject:ActionBar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my_sites)
setSupportActionBar(findViewById(R.id.my_toolbar))
sites = ArrayList(db.appDao().getAllSites()) //From Database
gridView = findViewById<View>(R.id.gridview) as GridView
siteAdapter = SitesAdapter(this#MySitesActivity, sites)
gridView.adapter = siteAdapter
gridView.choiceMode = GridView.CHOICE_MODE_MULTIPLE // CAN DO IN XML
actionBarObject = supportActionBar!!
actionBarObject.setDisplayHomeAsUpEnabled(true)
Log.d(TAG, "* Setting MultiChoiceModeListener *")
gridView.setMultiChoiceModeListener(object : AbsListView.MultiChoiceModeListener {
override fun onItemCheckedStateChanged(mode: ActionMode, position: Int,
id: Long, checked: Boolean) {
// Here you can do something when items are selected/de-selected,
// such as update the title in the CAB
}
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
// Respond to clicks on the actions in the CAB
return false
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
// Inflate the menu for the CAB
Log.d(TAG,"Inflating menu")
mode.menuInflater.inflate(R.menu.delete_menu, menu)
return true
}
override fun onDestroyActionMode(mode: ActionMode) {
// Here you can make any necessary updates to the activity when
// the CAB is removed. By default, selected items are deselected/unchecked.
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
// Here you can perform updates to the CAB due to
// an invalidate() request
return false
}
})
}
}
For brevity, I've removed the databse functions. The onCreate Action Mode never gets called and the Log statement never spits out the data.
My SiteAdapter Class extends the Baseadapter and the view returned is fine and it shows fine. I am not adding any listener or anything like that in my siteadapter class. Its just that on long click nothing happens. Any ideas as what could I be doing wrong? Thanks
Ok, So I've chosen the wrong mode.
GridView.CHOICE_MODE_MULTIPLE instead of GridView.CHOICE_MODE_MULTIPLE_MODAL
This change solve the INITIAL problem and created a new one which I think I should post it on a seperate thread.
I have an Activity with two child fragments Timeline and Milestones. Both these fragments contain listviews populated by a custom Cursor adapter
Here is a graphical Representation:
Now when I am on TIMELINE and I open up the searchview, I type something all is good I get the desired result. But when I navigate from Timeline to Milestones with some text in the searchview the searchview does not get cleared, so I get filtered results on the Milestones page too and acccording to the paramaters I provided in Timeline.
I am using AppCompact lib to develop my ActionBar. The tabs in there are not ActionBar Tabs but simple SlidingTabLayout.
So far I have tried using
getActivity().supportInvalidateOptionsMenu(); in onResume() of both the fragments, does not work.
I have tried searchView.setQuery("",false) - does not work and randomly gives me a NPE.
SO what do I miss here?
You can take a look on my example, where I showed how to control searchView between fragments.
Firstly. You need to create BaseFragment, which works with context of activity with appBarLayout.
open class BaseFragment: Fragment() {
lateinit var rootActivity: MainActivity
lateinit var appBarLayout: AppBarLayout
lateinit var searchView: androidx.appcompat.widget.SearchView
override fun onAttach(context: Context) {
super.onAttach(context)
this.rootActivity = context as MainActivity
appBarLayout = rootActivity.findViewById(R.id.app_bar_layout)
searchView = rootActivity.findViewById(R.id.search_input)
}
override fun onResume() {
super.onResume()
resetAppBarLayout()
}
private fun resetAppBarLayout() {
appBarLayout.elevation = 14f
}
fun setupSearch(query: String) {
searchView.visibility = View.VISIBLE
searchView.clearFocus()
when(query.isNotEmpty()) {
true -> {
searchView.setQuery(query, true)
searchView.isIconified = false
}
false -> {
searchView.isIconified = true
searchView.isIconified = true
}
}
}
fun hideSearchKeyboard() {
context?.let {
KeyboardHelper.hideSearchKeyboard(it, searchView.findViewById(R.id.search_src_text))
}
}
fun hideSearch() {
searchView.visibility = View.GONE
searchView.clearFocus()
}
}
Secondly. Inherit your fragments from BaseFragment, override onResume() method and control searchView in your fragments by calling methods from BaseFragment. Something like this.
class FragmentA : BaseFragment() {
private var searchQuery = ""
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.fragment, container, false)
val textView: TextView = root.findViewById(R.id.textView)
textView.text = "Fragment A"
return root
}
override fun onResume() {
super.onResume()
setupSearch()
}
private fun setupSearch() {
searchView.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextChange(newText: String?): Boolean {
when(newText.isNullOrEmpty()) {
true -> searchQuery = ""
false -> searchQuery = newText
}
return true
}
override fun onQueryTextSubmit(query: String?): Boolean {
hideSearchKeyboard()
return true
}
})
super.setupSearch(searchQuery)
}
}
Full example you can find here https://github.com/yellow-cap/android-manage-searchview