I am trying to pass click event on a view in a Fragment to the Activity. I was following documentation guide from here.
As I understand it and I could be wrong but we need to create an interface, add a method declaration to it and trigger the method from when the click event is received on the Fragment. The Activity should then implement the interface defined in the fragment, so that the activity receives that event.
I have my Fragment:
class MoreFragment : Fragment() {
internal var callback: OnMoreItemClickedListener
fun setOnMoreItemClickedListener(callback: OnMoreItemClickedListener) {
this.callback = callback
}
interface OnMoreItemClickedListener {
fun onAddClothClicked()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_more, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
}
override fun onStart() {
super.onStart()
btn_add_clothes.setOnClickListener {
callback.onAddClothClicked()
}
}
}
And I have my Activity:
class MainActivity : AppCompatActivity(), MoreFragment.OnMoreItemClickedListener {
override fun onAddClothClicked() {
Log.d("MainActivity", "HERE")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navView: BottomNavigationView = findViewById(R.id.nav_view)
val navController = findNavController(R.id.nav_host_fragment)
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
val appBarConfiguration = AppBarConfiguration(
setOf(
R.id.navigation_clothes,
R.id.navigation_seasons,
R.id.navigation_notifications,
R.id.navigation_more
)
)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
}
override fun onAttachFragment(fragment: Fragment) {
super.onAttachFragment(fragment)
if(fragment is MoreFragment){
fragment.setOnMoreItemClickedListener(this)
}
}
}
Now the error that I am getting is:
MoreFragment.kt: (16, 5): Property must be initialized or be abstract
MoreFragment.kt: (16, 5) is internal var callback: OnMoreItemClickedListener.The doc link above does suggest the same/ similar code but it does not work.
Things that I have tried
I have tried putting a lateinit var for the callback: OnMoreItemClickedListener variable there but no luck. As it never gets initialized.
I put internal var callback: OnMoreItemClickedListener? = null and referenced callback using the null safe operator ?. but was not able to get the click event.
I am fairly new to Kotlin and Fragments too so please help me figure how to do this. Thanks
In Fragment override onAttach method
private lateinit var callback: OnMoreItemClickedListener
override onAttach(context : Context){
callback = context as OnMoreItemClickedListener
}
then your callback will be init. as onAttach will be first. so no Property must be initialized or be abstract errror will occur.
also in Activity implement your interface(OnMoreItemClickedListener) and override its method. You will get callback
Your both way is looks OK
I have tried putting a lateinit var for the callback:
I put internal var callback: OnMoreItemClickedListener? =
null
I guess issue is here just debug below code. is it going inside if
if(fragment is MoreFragment){
fragment.setOnMoreItemClickedListener(this)
}
Related
So my problem is that I created a OnPageChangeCallback (successfully) BUT i am not sure how to unregister it to avoid memory leak...
this is my mainActivity file everything is working properly otherwise :
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
lateinit var viewPager: ViewPager2
lateinit var navigationView: BottomNavigationView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
viewPager = binding.viewPager
val adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
viewPager.adapter = adapter
navigationView = binding.bottomNavigation
navigationView.setOnItemSelectedListener { item ->
when(item.itemId){
R.id.nav_home -> viewPager.currentItem = 0
R.id.nav_info -> viewPager.currentItem = 1
}
true
}
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback(){
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
when(position){
0 -> navigationView.menu.findItem(R.id.nav_home).isChecked = true
1 -> navigationView.menu.findItem(R.id.nav_info).isChecked = true
}
}
})
}
// I am not sure I should do it this way
override fun onDestroy() {
super.onDestroy()
// I don't know how to put the callback in parameter (the ?? marks)
viewPager.unregisterOnPageChangeCallback(??)
}
}
If anyone can help me I want to learn how to do things properly I went to the android documentation but I didn't understand how to do it :/
As for Pager documentation
Remove a callback that was previously added via registerOnPageChangeCallback(ViewPager2.OnPageChangeCallback).
Params:
callback – callback to remove
So to remove callback you'll need to save your ViewPager2.OnPageChangeCallback() in a variable and then pass it to unregisterOnPageChangeCallback in OnDestroy lifecycle method.
I have an app with 3 main destinations which can be accessed by a bottom nav view. Each destination has its own navigation graph.
The problem is that when I minimize and reopen my app, the navigation components reset to the default destination. Why does this happen?
My main activity: (Irrelevant code omitted)
class MainActivity : AppCompatActivity() {
// List of base host containers
val fragments = listOf(
HomeHostFragment(),
CoursesHostFragment(),
SearchHostFragment()
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewPager: ViewPager = findViewById(R.id.main_activity_pager)
viewPager.adapter = ViewPagerAdapter()
}
inner class ViewPagerAdapter : FragmentPagerAdapter(supportFragmentManager) {
override fun getItem(position: Int): Fragment =
fragments[position]
override fun getCount(): Int =
fragments.size
}
}
HomeHostFragment.kt:
class HomeHostFragment : Fragment() {
private lateinit var navController: NavController
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_home_host, container, false)
override fun onStart() {
super.onStart()
navController = requireActivity().findNavController(R.id.nav_host_home)
navController.setGraph(R.navigation.nav_graph_home)
NavigationUI.setupWithNavController(toolbar_home, navController)
}
fun onBackPressed(): Boolean {
return navController.navigateUp()
}
}
Whenever onStart() is called, NavigationUI.setupWithNavController() is called again which resets the navigation. Move this call to onViewCreated() so that the navigation setup is not done every time the fragment pauses and restarts.
I want to add my custom layout to the side of the existing Toolbar, keeping all features with NavigationUI (Up button, animations, placing up buttons and drawers, etc)
How can i achieve this?
After some research i`ve found out the solution
MainActivity.kt
class MainActivity : AppCompatActivity() {
private val navController by lazy { findNavController(R.id.main_nav_host_fragment) }
private val appBarConfiguration by lazy { AppBarConfiguration(navController.graph) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navController = this.findNavController(R.id.main_nav_host_fragment)
NavigationUI.setupActionBarWithNavController(this, navController)
NavigationUI.setupWithNavController(
findViewById<NavigationView>(R.id.navigation_view),
navController
)
}
override fun onSupportNavigateUp(): Boolean {
val navController = this.findNavController(R.id.main_nav_host_fragment)
return NavigationUI.navigateUp(navController, appBarConfiguration)
}
}
FragmentToolbar.kt interface
interface FragmentToolbar {
fun clearActionBar()
fun setCustomActionBar()
}
MainFragment.kt
Next class MainFragment.kt is abstract with methods clearActionBar() and setCustomActionBar() that can be overridden. They are defined in FragmentToolbar.kt interface, because if you set custom layout in first fragment, you will see it also in all other fragments. So, you will almost always have to clear your ActionBar and this class is responsible for standard realisation. setCustomActionBar() is up to you\
abstract class MainFragment : Fragment(), FragmentToolbar {
val actionBar: ActionBar? by lazy { (requireActivity() as AppCompatActivity).supportActionBar }
override fun clearActionBar() {
actionBar?.apply {
setDisplayShowCustomEnabled(false)
setDisplayShowTitleEnabled(true)
}
}
override fun setCustomActionBar() {}
}
MyFragment.kt
class MyFragment : MainFragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
clearActionBar() // defined in MainFragment
// Your code
}
override fun setCustomActionBar() {
actionBar?.apply {
setDisplayShowCustomEnabled(true)
setCustomView(R.layout.view_cart_and_bill)
}
actionBar?.customView?.apply { /* Here use findViewById to find necessary views inside your custom layout */ }
}
}
Hope my answer helps you. It also would be great, if ViewBinding could be used with this layout added in runtime, but i have not solved this issue yet
I am a newbie Android developer, and I am trying to observe a boolean set in the ViewModel from its parent's activity. I can observe its initial state as soon as the app launches, but any change applied later on doesn't seem to trigger the observer (i.e. when I switch the fragments).
Here is the code for my ViewModel:
class MyMusicViewModel : ViewModel() {
private var _MyMusicViewOn = MutableLiveData<Boolean>()
val MyMusicViewOn: LiveData<Boolean> get() = _MyMusicViewOn
init {
Timber.i("MyMusicViewModel Init Called!")
setMyMusicView(true)
}
override fun onCleared() {
super.onCleared()
Timber.i("MyMusicViewModel Cleared!")
setMyMusicView(false)
}
fun setMyMusicView(setter: Boolean) {
Timber.i("MyMusicViewModel setter called! %s", setter)
_MyMusicViewOn.value = setter
}
}
And here is its parent's activity:
class FullscreenActivity : AppCompatActivity() {
private val viewModel: MyMusicViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.MyMusicViewOn.observe(this, Observer { MyMusicViewOn ->
Timber.i("Observer called for MyMusicViewOn %s", MyMusicViewOn)
})
}
}
And in case you wanna see the ViewModel's related fragment, here it is:
class MyMusicFragment : Fragment() {
private lateinit var viewModel: MyMusicViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val binding = DataBindingUtil.inflate<FragmentMyMusicBinding>(
inflater,
R.layout.fragment_my_music,
container,
false
)
viewModel = ViewModelProvider(this).get(MyMusicViewModel::class.java)
return binding.root
}
override fun onResume() {
super.onResume()
Timber.i("MyMusicViewFragment resumed!")
viewModel.setMyMusicView(true)
}
}
What I am trying to accomplish is to observe the onResume(), onCleared() and init{} functions whenever they are called by changing the status of the MyMusicViewOn MutableLiveData Boolean. What I don't understand is why that boolean doesn't trigger the observer set in the parent activity whenever it changes.
Thankyou in advance for any thoughts!
All the best,
Fab.
I'm guessing that however you are populating that viewModel property in your Fragment, you are not using the Activity's ViewModel instance. The easiest way to get the same instance that the Activity is using would be to use the activityViewModels delegate:
private val viewModel: MyMusicViewModel by activityViewModels()
I use Clean Architecture, LiveData, Navigation component & Bottom Navigation view.
I am creating a simple application with three tabs. By default, the First tab Fragment loads user data using some API. When i go to another tabs and then return to the First tab Fragment, i see, that observe return a new data!
I need observe not to return data again when I switch back to the first tab! what am I doing wrong? Could you help me please?
P.s. For navigation i use sample from navigation-advanced-sample and after switching tabs onDestroy is not called.
First solution in the article Observe LiveData from ViewModel in Fragment said:
One proper solution is to use getViewLifeCycleOwner() as LifeCycleOwer while observing LiveData inside onActivityCreated as follows.
I use following code, but it's not work for me:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
Timber.d("onActivityCreated")
viewModel.getProfileLive().observe(viewLifecycleOwner, observer)
}
Second solution in the article Architecture Components pitfalls — Part 1 recommends using Resetting an existing observer and Manually unsubscribing the observer in onDestroyView(). But it doesn't work for me either...
ProfileFragment.kt
class ProfileFragment : DaggerFragment() {
#Inject
lateinit var viewModel: ProfileFragmentViewModel
private val observer = Observer<Resource<Profile>> {
when (it.status) {
Resource.Status.LOADING -> {
Timber.i("Loading...")
}
Resource.Status.SUCCESS -> {
Timber.i("Success: %s", it.data)
}
Resource.Status.ERROR -> {
Timber.i("Error: %s", it.message)
}
}
};
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Timber.d("onCreate")
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Timber.d("onCreateView")
return inflater.inflate(R.layout.fragment_profile, container, false)
}
fun <T> LiveData<T>.reObserve(owner: LifecycleOwner, observer: Observer<T>) {
removeObserver(observer)
observe(owner, observer)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Timber.d("onViewCreated")
viewModel.getProfileLive().observe(viewLifecycleOwner, observer)
// viewModel.getProfileLive().reObserve(viewLifecycleOwner, observer)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
Timber.d("onActivityCreated")
}
override fun onDestroyView() {
super.onDestroyView()
Timber.d("onDestroyView")
// viewModel.getProfileLive().removeObserver(observer)
}
override fun onDestroy() {
super.onDestroy()
Timber.d("onDestroy")
}
override fun onDetach() {
super.onDetach()
Timber.d("onDetach")
}
}
ProfileFragmentViewModel.kt
class ProfileFragmentViewModel #Inject constructor(
private val profileUseCase: ProfileUseCase
) : ViewModel() {
init {
Timber.d("Init profile VM")
}
fun getProfileLive() = profileUseCase.getProfile()
}
ProfileUseCase
class ProfileUseCase #Inject constructor(
private val profileRepository: ProfileRepository
) {
fun getProfile(): LiveData<Resource<Profile>> {
return profileRepository.getProfile()
}
}
ProfileRepository.kt.
class ProfileRepository #Inject constructor(
private val loginUserDao: LoginUserDao,
private val profileDao: ProfileDao,
) {
fun getProfile(): LiveData<Resource<Profile>> =
liveData(Dispatchers.IO)
{
emit(Resource.loading(data = null))
val profile = profileDao.getProfile()
// Emit Success result...
}
}
It's because of how Fragment Lifecycle works. When you move to and fro from a fragment onViewCreated() is called again. In onViewCreated you're calling viewModel.getProfileLive() which returns the livedata upto from the repository and observe to it.
Since onViewCreated() gets called everytime when you move back to the Fragment so is your call to viewModel.getProfileLive() and in turn the repository gets called again which again triggers the observe method in your Fragment.
In order to solve this problem,
create a LiveData variable in your ViewModel, set it to the returned Live Data from Repository.
In the Fragment observe to the LiveData variable of your ViewModel not the one returned from Repository.
That way, your observe method will get triggered on very first time and only when value of your data from repository changes.