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
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()
}
}
When I search on my searchview I want to send the search value to the api that will give back a response that I want to be showed on the fragment. So when I submit my search I want to show the fragment with the response! I tried to make a function to render the fragment but I think im doing it completly wrong...
Im begginer and this is for a project for school, thank you for help!
SearchView
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.main, menu)
val manager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem?.actionView as SearchView
searchView.setSearchableInfo(manager.getSearchableInfo(componentName))
searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener{
override fun onQueryTextSubmit(query: String?): Boolean {
searchView.clearFocus()
searchView.setQuery("",false)
searchItem.collapseActionView()
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
return false
}
})
return true
}
Data Class
data class SearchPost(val searchKey: String)
Fragment
class SendFragment : Fragment() {
var newList: MutableList<News> = mutableListOf<News>()
companion object {
fun newInstance() = SendFragment()
}
private lateinit var viewModel: SendViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_searched, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(SendViewModel::class.java)
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = LinearLayoutManager(this.context)
// TODO: Use the ViewModel
val searchedObserver = Observer<List<News>>
{
// Access the RecyclerView Adapter and load the data into it
newList -> recyclerView.adapter = NewsAdapter(newList,this.context!!)
}
viewModel.getNewSearched().observe(this, searchedObserver)
}
}
Fragment View Model
class SendViewModel : ViewModel() {
// TODO: Implement the ViewModel
private var newList: MutableLiveData<List<News>> = MutableLiveData()
fun getNewSearched(): MutableLiveData<List<News>>
{
searchedNew()
return newList;
}
private fun searchedNew()
{
val retrofit = Retrofit.Builder()
.baseUrl("http://192.168.1.78:3000")
.addConverterFactory(GsonConverterFactory.create())
.build()
val api = retrofit.create(ApiService::class.java)
val searchPost = SearchPost("this is want to be the query")
api.sendSearch(searchPost).enqueue(object : Callback<List<News>> {
override fun onResponse(call: Call<List<News>>, response: Response<List<News>>) {
newList.value=(response.body()!!)
}
override fun onFailure(call: Call<List<News>>, t: Throwable) {
Log.d("fail", "onFailure:")
}
})
}
}
Api interface
interface ApiService {
#POST("/search")
fun sendSearch(#Body searchPost: SearchPost): Call<List<News>>
}
Observe view model like this :
viewModel.getNewSearched().observe(this, Observer<MutableList<List<News>>> {
myNewsData ->
Log.d("print my data", myNewsData) // first try to print this data whether data is coming or not
recyclerView.adapter = NewsAdapter( myNewsData ,this.context!!)
})
Initialize your searchView :
private fun initSearchView() {
search_view.setOnQueryTextListener(object :
androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(newText: String?): Boolean {
viewMode.getNewSearched() // call your api here
return false // don,t change it to true
}
override fun onQueryTextChange(query: String?): Boolean {
return false
}
})
}
At last I thing you should pass the text to viewmodel which you want to search
override fun onQueryTextSubmit(query: String?): Boolean {
viewMode.getNewSearched(query)
In side viewmodel :
fun getNewSearched(textYouWantToSearch :String): MutableLiveData<List<News>>
{
searchedNew(textYouWantToSearch) // same pass in searchedNew() else your data class is always blank
return newList;
}
In my understanding your data class which you are passing in your retrofit call is always blank because you are not passing any value from anywhere you should use query string of your search method and try to pass as an parameter.
I'm new on Android, in particular on Kotlin development.
How from title, i'm trying to understand how to achieve this:
I have an Activity with some buttons and textviews. I would to implement an hidden fragment opened after 5 clicks on UI. That fragment work look like the activity. I'm able to open the fragment properly and set the layout properly. I don't know how to replace buttons activity settings from activity to fragment. I have same problem with the textview. How could I achieve it?
Thanks in Advance.
Here Activity Kotlin part that open fragment:
override fun onTouchEvent(event: MotionEvent): Boolean {
var eventaction = event.getAction()
if (eventaction == MotionEvent.ACTION_UP) {
//get system current milliseconds
var time = System.currentTimeMillis()
//if it is the first time, or if it has been more than 3 seconds since the first tap ( so it is like a new try), we reset everything
if (startMillis == 0L || (time-startMillis> 3000) ) {
startMillis=time
count=1
}
//it is not the first, and it has been less than 3 seconds since the first
else{ // time-startMillis< 3000
count++
}
if (count==5) {
// Log.d("tag","start hidden layout")
// Get the text fragment instance
val textFragment = MyFragment()
val mytostring =board_status_tv.toString()
val mArgs = Bundle()
mArgs.putString(BOARDSTATE, mytostring)
textFragment.setArguments(mArgs)
// Get the support fragment manager instance
val manager = supportFragmentManager
// Begin the fragment transition using support fragment manager
val transaction = manager.beginTransaction()
// Replace the fragment on container
transaction.replace(R.id.fragment_container,textFragment)
transaction.addToBackStack(null)
// Finishing the transition
transaction.commit()
}
return true
}
return false
}
Fragment Kotlin class:
class MyFragment : Fragment(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val parentViewGroup = linearLayout
parentViewGroup?.removeAllViews()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Get the custom view for this fragment layout
val view = inflater!!.inflate(R.layout.my_own_fragment,container,false)
// Get the text view widget reference from custom layout
val tv = view.findViewById<TextView>(R.id.text_view)
// val tv1 = view.findViewById<TextView>(R.id.board_status_tv1)
// Set a click listener for text view object
tv.setOnClickListener{
// Change the text color
tv.setTextColor(Color.RED)
// Show click confirmation
Toast.makeText(view.context,"TextView clicked.",Toast.LENGTH_SHORT).show()
}
// Return the fragment view/layout
return view
}
override fun onPause() {
super.onPause()
}
override fun onAttach(context: Context?) {
super.onAttach(context)
}
override fun onDestroy() {
super.onDestroy()
}
override fun onDetach() {
super.onDetach()
}
override fun onStart() {
super.onStart()
}
override fun onStop() {
super.onStop()
}
}
Please note that you will need to get Text before converting it to string, like that in second line.
board_status_tv .getText(). toString()
val textFragment = MyFragment()
val mytostring = board_status_tv.getText().toString()
val mArgs = Bundle()
mArgs.putString(BOARDSTATE, mytostring)
textFragment.setArguments(mArgs)
Hope this will resolve your problem
I am using Kotlin Android Extension to access view directly by their id.
I have a progress bar which I access directly in fragment using id i.e progress_bar
<ProgressBar
android:id="#+id/progress_bar"
style="#style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="15dp"
android:indeterminate="true"/>
In fragment, I am showing and hiding it with this code
progress_bar.visibility = if (visible) View.VISIBLE else View.GONE
It is working perfectly until I rotate the screen. After that, it throws the exception
java.lang.IllegalStateException: progress_bar must not be null.
The variable gets null on screen rotation. How to solve this problem?
Fragment code
class SingleAppFragment : Fragment() {
private lateinit var appName: String
companion object {
fun newInstance(appName: String = ""): SingleAppFragment {
val fragment = SingleAppFragment()
val args = Bundle()
args.putString(Constants.EXTRA_APP_NAME, appName)
fragment.arguments = args
return fragment
}
}
private var mListener: OnFragmentInteractionListener? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
appName = if (arguments != null && !arguments.getString(Constants.EXTRA_APP_NAME).isEmpty()) {
arguments.getString(Constants.EXTRA_APP_NAME)
} else {
Constants.APP_NAME_FACEBOOK
}
}
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater!!.inflate(R.layout.fragment_single_app, container, false)
}
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
setEventListeners()
}
private fun initView() {
var canShowSnackBar = true
web_single_app.webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
showHideProgressBar(true)
canShowSnackBar = true
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
showHideProgressBar(false)
}
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
web_single_app.stopLoading()
if (canShowSnackBar) {
mListener?.onErrorWebView()
canShowSnackBar = false
}
}
}
web_single_app.settings.javaScriptEnabled = true
web_single_app.loadUrl(Constants.APP_NAME_URL_MAP[appName])
}
private fun setEventListeners() {
back_web_control.setOnClickListener({
web_single_app.goBack()
})
}
fun showHideProgressBar(visible: Boolean) {
progress_bar_web_control.visibility = if (visible) View.VISIBLE else View.GONE
}
fun loadUrl(appName: String) {
web_single_app.loadUrl(Constants.APP_NAME_URL_MAP[appName])
}
override fun onAttach(context: Context?) {
super.onAttach(context)
if (context is OnFragmentInteractionListener) {
mListener = context
}
}
override fun onDetach() {
super.onDetach()
mListener = null
}
interface OnFragmentInteractionListener {
fun onErrorWebView()
}
}
Steps to reproduce:
Start Activity
Fragment get loaded
At Fragment load, I load an URL and show a progress bar
At loading the URL I rotate the phone and the progress bar variable gets null
In my case this bug happens from time to time. Of course, onViewCreated() is a good method to place your code in. But sometimes it's strangely not enough. And setRetainInstance(true) may help, may not. So sometimes this helps: access your Views with a view variable. You can even access them inside onCreateView(). You can use ?. for a guarantee that an application won't crash (of course, some views won't update in this case). If you wish to get context, use view.context.
In my case this bug reproduced only in Kotlin coroutines.
private fun showProgress(view: View) {
view.progressBar?.visibility = View.VISIBLE
}
private fun hideProgress(view: View) {
view.progressBar?.visibility = View.GONE
}
Then in code:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
showData(view)
}
private fun showData(view: View) {
showProgress(view)
adapter = SomeAdapter()
adapter.setItems(items)
val divider = SomeItemDecoration(view.context)
view.recycler_view?.run {
addItemDecoration(divider)
adapter = this#SomeFragment.adapter
layoutManager = LinearLayoutManager(view.context)
setHasFixedSize(true)
}
hideProgress(view)
}
In which method do you get the progress_bar by Id?
Please consider the fragment state lifecycle. Maybe you try to load it when the view is not ready yet.
Ensure your progress_bar variable is assigned only after the view is ready. For example in the
onViewCreated method.
See here the official Android lifecycle:
Update
As #CoolMind pointed out the diagram doesn't show the method onViewCreated.
The complete Android Activity/Fragment lifecycle can be found here:
Add retain intance true to the fragment so that it will not be destroyed when an orientation changes occurs
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance=true
}
Also do a null check using safe call operator before accessing views
fun showHideProgressBar(visible: Boolean) {
progress_bar_web_control?.visibility = if (visible) View.VISIBLE else View.GONE
}