There is a specific implementation of interface when it comes to using Kotlin for Android: triggering an interface from a fragment. Consider the common scenario where a Fragment must communicate a UI action to the parent Activity. In Java, we would define the interface, create a "listener" instance of it, and implement/override actions in interface parents. It is the creating a listener instance that is not so straightforward to me. After some googling, I found an implementation example but I do not understand why it works. Is there a better way to do this? Why must it be implemented the way it is below?
class ImplementingFragment : Fragment(){
private lateinit var listener: FragmentEvent
private lateinit var vFab: FloatingActionButton
//Here is the key piece of code which triggers the interface which is befuddling to me
override fun onAttach(context: Context?) {
super.onAttach(context)
if(context is FragmentEvent) {
listener = context
}
}
//Notice the onClickListener using our interface listener member variable
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val v: View? = inflater.inflate(R.layout.fragment_layout, container, false)
vFab = v?.findViewById(R.id.fh_fab)
vFab.setOnClickListener{listener.SomethingHappened()}
return v
}
}
interface FragmentEvent{
fun SomethingHappened()
}
In this code lateinit var listener: FragmentEvent should be initialized and can't be null. So onAttach should looks like
override fun onAttach(context: Context?) {
super.onAttach(context)
if (context is FragmentEvent) {
listener = context
} else {
throw RuntimeException(context!!.toString() + " must implement FragmentEvent")
}
}
In this case if you forget to implement FragmentEvent in Activity you'll got an exception, otherwise callback is ready to use.
you could also use a try catch block and get the classic Java pattern
override fun onAttach(context: Context) {
super.onAttach(context)
try {
listener = context as YourInterface
} catch (e: IllegalStateException) {
Log.d("TAG", "MainActivity must implement YourInterface")
}
}
the 'as' keyword can be used in to replicate java explicit typecasting in kotlin
val customItem = arguments?.getSerializable("KEY") as ArrayList<Custom>
Related
I'm using a interface to switch from recycler View to details activitas. my interface function works. position is coming. But I can't switch to Details Activity. I think the soproduct is from context. How can I solve this problem? Thank you
class OrderFragment : Fragment() , OnMovieClickListener {
private lateinit var linearLayoutManager: LinearLayoutManager
private lateinit var adapter: RvAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view : View = inflater.inflate(R.layout.fragment_order, container, false)
val orderApiService = OrderApiService()
val api = orderApiService.getDataa(requireActivity())
api.myOrdersAssigned().enqueue(object : Callback<List<BaseModel>?> {
override fun onResponse(
call: Call<List<BaseModel>?>,
response: Response<List<BaseModel>?>
) {
val arrayOrder = response.body()
val layoutManager: LinearLayoutManager = LinearLayoutManager(activity)
recyclerViewMyOrders.setLayoutManager(layoutManager)
adapter = RvAdapter(arrayOrder as ArrayList<BaseModel>,this#OrderFragment)
recyclerViewMyOrders.setAdapter(adapter)
adapter.notifyDataSetChanged()
if(response.isSuccessful){
response.body()?.let {
}
}
}
override fun onFailure(call: Call<List<BaseModel>?>, t: Throwable) {
print(t.message.toString())
}
})
return view
}
override fun onMovieItemClicked(position: Int) {
println("Clicked : " + position.toString())
val intent = Intent(requireContext().applicationContext,DetailsActivity::class.java)
startActivity(intent)
}
}
My Interface Function :
override fun onMovieItemClicked(position: Int) {
println("Clicked : " + position.toString())
val intent = Intent(requireContext().applicationContext,DetailsActivity::class.java)
startActivity(intent)
}
Create your variable Context inside your Fragment like this :
private Context context;
Than you initialize it inside onCreateView like this :
context = view.getContext();
And insite you functio, instead of calling requireContext().applicationContext as parameter, call context
The code in on Java but you can easily convert it to Kotlin
I have a question first: why don't you use the DetailFragment instead of a whole new Activity (because your OrderFragment and Detail can be both in a single Activity)
Still want to use Activity
On your interface's override method, try to change requireContext().applicationContext to requireContext() only. Please reply me your println works or not.
Change to Fragment
I know 2 ways to move to another fragment programmatically:
parentFragmentManager.beginTransaction()
.replace<DestinationFragment>(R.id.fragmentContainer)
findNavController().navigate(action) // if you are using Navigation Components
P/s:
I'm also a Android newbie and I see something can be improved in your source code, please correct me if I have something wrong
Adapter and LinearLayoutManager don't have to be a class's property because you no longer need to use them outside API call, so just change their scope to inside onResponse()
I have suffered from doing things with view in onCreateView() (because they are not fully inflated yet ?), so considering move your logic to onViewCreated() for a safe bet.
Im working on a project and implementing the MVVM model with databinding and navigation. My button is on a fragment that opens with a drawer menu item, the thing is when i click on the button it does nothing, the debugger doesn't go into the navigate method, I really don't know what I did wrong, can someone help?
MYACCOUNT CLASS:
class MyAccountFragment : BaseFragment() {
private val vm: MyAccountViewModel by viewModel()
override fun getViewModel(): BaseViewModel = vm
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentMyAccountBinding.inflate(inflater, container, false)
context ?: return binding.root
injectFeature()
setToolbar(binding)
subscribeUi(binding)
return binding.root
}
/**
* set toolbar
* **/
private fun setToolbar(binding: FragmentMyAccountBinding) {
binding.appBarLayout.backClickListener = (activity as MainActivity).createOnBackClickListener()
}
/**
* set ui
* **/
private fun subscribeUi(binding: FragmentMyAccountBinding) {
binding.viewModel = vm
}
}
MYACCVIEWMODEL
class MyAccountViewModel constructor() : BaseViewModel() {
fun onAddRoomClick()
{
navigate(MyAccountFragmentDirections.actionMyAccountFragmentToAddRoomFragment())
}
}
and in the xml i implemented the
android:onClick="#{() -> viewModel.onAddRoomClick()}"
Im using this pattern for all my Fragments and ViewModels, and i really dont know why it doesn't do anything, the vm initializes. On the other drawermenu fragment I also have the onClick method and it navigates to the other fragment. So if anyone knows the solution that would be helpful, thank you in advance.
the answer was in the initialization of the viewModel.
the onClick method in xml is in a content_layout that is included in a fragment_layout and instead of binding.viewModel = vm I had to do binding.content_layout.viewModel = vm.
private fun subscribeUi(binding: FragmentMyAccountBinding) {
binding.contentMyAccount.viewModel = vm
}
ViewModel is not supposed to handle any kind of navigation, it will just receive the event from the UI and pass it to the controller (which might be a fragment or activity) and the latter will handle the navigation...
So one way to solve your issue is to do the following:
ViewModel
class MyAccountViewModel constructor() : BaseViewModel() {
private val _navigateToRoomFragEvent = MutableLiveData<Boolean>(false)
val navigateToRoomFragEvent:LiveData<Boolean>
get()=_navigateToRoomFragEvent
fun onAddRoomClick()
{
_navigateToRoomFragEvent.value=true
}
fun resetNavigation(){
_navigateToRoomFragEvent.value=false
}
}
Controller (Activity or Fragment)
inside **onCreate() (if it is an activity)**
viewModel.navigateToRoomFragEvent.observe(this,Observer{navigate->
//boolean value
if(navigate){
navController.navigate(//action)
}
viewModel.resetNavigation() //don't forget to reset the event
})
onActivityCreated(if it is a fragment)
viewModel.navigateToRoomFragEvent.observe(viewLifeCycleOwner,Observer{navigate->
//boolean value
if(navigate){
navController.navigate(//action)
}
viewModel.resetNavigation() //don't forget to reset the event
})
Hope it helps,
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)
}
I have a project in which I have to build views inside a custom Layout. This layout represents the concept of View in MVP architecture and it lives in a Fragment. The view should be updated by the Presenter whenever an event happens, by calling the View and finally that will update TextViews inside the View. But it seems that after the View is initialized, nothing gets updated anymore.
If my presenter calls the View that contains my TextView - nothing. If I try to update the TextView directly, from the fragment then it works. I can't really understand what is happening and why it doesn't get updated from within the layout that contains that TextView.
MyCustomView:
class MyCustomView(fragment: MyFragment): MyViewInterface, FrameLayout(fragment.context) {
init {
View.inflate(context, R.layout.my_fancy_layout, this)
}
override fun getView(): View {
return this
}
override fun setData(uiModel: UiModel) {
textview_name.text = uiModel.name
}
}
MyFragment:
class MyFragment : Fragment() {
#Inject lateinit var view: MyViewInterface
#Inject lateinit var presenter: MyCustomPresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
... dagger injection ...
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return this.view.getView()
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.setData(...some ID to fetch data from API...)
//textview_name.text = "blue" //this works instead
}
}
MyPresenter:
class MyPresenter #Inject constructor(
private val repo: MyRepository,
private val view: MyViewInterface
) {
fun setData(productCode: String) {
.. some code ...
view.setData(it) //call to my view
}
}
MyViewInterface:
interface MyViewInterface {
fun getView(): View
fun setData(uiModel: UiModel)
}
All I can think of is the view's instance is not the same in your UI and presenter. I dont know your Dagger's code so I do not have any suggestions to fix it.
You could move the view away from MyPresenter's constructor and set it in MyFragment.onCreate after injection.
Because of you just inflate when create view
init {
View.inflate(context, R.layout.my_fancy_layout, this)
}
add invalidate view in update data function
override fun setData(uiModel: UiModel) {
textview_name.text = uiModel.name
this.invalidate()
this.requestLayout()
}
Is there any better clean way to put a listener inside a Fragment?
class FooFragment : Fragment() {
companion object {
private lateinit var listener: (Int) -> Unit
fun newInstance(listener: (Int) -> Unit): FooFragment {
Companion.listener = listener
return FooFragment()
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_foo, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btn.setOnClickListener {
listener(1234)
}
}
}
The call is so tidy...
FooFragment.newInstance {
// Do something
}
I know this code have problems due to maybe I lose the listener when the Fragment is recreated and that the standard way is using the onAttach(context: Context?) way.
But I want to create a new instance of the FooFragment from other Fragment (not an Activity). I could use something like this:
private var listener: Listener? = null
interface Listener {
fun onFooClicked()
}
override fun onAttach(context: Context?) {
super.onAttach(context)
// listener = context as Listener
fragmentManager?.findFragmentById(R.id.fFooParent) as Listener
}
But this piece of code inside the onAttach(context: Context?) method inside the FooFragment, would be counterproductive due to I do not want that the FooFragment knows anything of the class who insntantiates it. Moreover, I would like that it could be a Fragment or an Activity.
You can crate a baseActivity which implements the listener
class BaseFragmentActivity:AppCompatActivity(), FooFragment.Listener{}
then extend class YouFragmentActivity : BaseFragmentActivity
then it will make sure that every fragment hosting activity is equipped with container then you can get the reference as BaseFragmentActivity
You can replace AppCompatActivity with BaseActivity where BaseActivity :AppCompatActivity