in kotlin … I want to pass data to a custom popup window which extends dialogFragment so anyone here knows how can I pass data to such a fragment ?.
I have error every time I pass data to the constructor.
Please help.
pass data to the constructor
class PopUpClass : DialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
var v = inflater.inflate(R.layout.poplayout,container,false
return v
}
//tried to pass the data in the constructor and then handle it but did not work
Have a look at the dialog fragment documentation.
You need to create a function getInstance that passes your parameters through a bundle to the fragment. Like this:
static MyDialogFragment newInstance(int num) {
MyDialogFragment f = new MyDialogFragment(); // Supply num input as an argument.
Bundle args = new Bundle();
args.putInt("num", num);
f.setArguments(args);
return f;
}
Here is how you can do it in Kotlin, first declare a companion object with instance of your fragment class
companion object {
#JvmStatic //This can be avoided if you are in a complete Kotlin project
fun newInstance(content: String): PopUpClass {
val args = Bundle()
args.putString("content", content)
val fragment = PopUpClass()
fragment.arguments = args
return fragment
}
}
Inside onCreate() or onViewCreated() of your fragment you can receive the data like this
val dataPassed: String? = arguments?.getString("content")
Call the newInstance instead of constructor from your parent activity or fragment
Related
I have an app, which uses api calls, to get some data by Retrofit from the server. I've implemented SwiperRefreshLayout to allow user, to perform another call.
Currently, I'm struggling with clearing MutableLiveData collection, which stores response from the server. I'd like to clear that collection every time the OnRefreshListener will be triggered.
I've tried to "fill" the MutableLiveData with null (as it comes by default, right?) but since I've set the observable in OnCreateView to pass the data to the Adapter, after every refresh I got NullPointerException error.
How I may solve it? Should I do something like unobserving, and observing the response collection again, when OnRefreshListener is triggered? Here's some code:
ViewModel
var responseData = MutableLiveData<Model?>()
fun fetchData(baseCurrency: String, selectedCurrencies: String) {
viewModelScope.launch {
val response =
retrofitRepository.fetchHistoricalData(date, selectedCurrencies, baseCurrency)
response.enqueue(object : retrofit2.Callback<Model> {
override fun onResponse(
call: Call<HistoricalRatesModel>,
response: Response<HistoricalRatesModel>
) {
if (response.isSuccessful) {
responseData.value = response.body()
}
}
override fun onFailure(call: Call<HistoricalRatesModel>, t: Throwable) {
Log.i(TAG, "onFailure ERROR\n${t.message}")
}
})
}
}
Fragment
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentHistoricalRatesBinding.inflate(inflater, container, false)
val view = mBinding.root
mViewModel.responseData.observe(requireActivity(), androidx.lifecycle.Observer {
mAdapter = Adapter()
mAdapter?.setData(it!!.rates)
mBinding.hrv.layoutManager = LinearLayoutManager(this.context)
mBinding.hrv.adapter = mAdapter
})
mBinding.refreshContainer.setOnRefreshListener {
mBinding.refreshContainer.isRefreshing = false
}
return view
}
You're not handling that potential null value in your observer - in fact you're telling the compiler that it will never be null!
mViewModel.responseData.observe(requireActivity(), androidx.lifecycle.Observer {
...
mAdapter?.setData(it!!.rates)
})
How you handle it depends on what you want to do when that null value is pushed to observers. If you want to clear the data in the adapter, you could do:
mAdapter?.setData(it?.rates ?: emptyList<Rate>())
or you could make your Adapter's setData() function accept null (and decide internally how to handle that) and then you can just do:
mAdapter?.setData(it?.rates)
If any of that's confusing, make sure you're familiar with the null safety features in Kotlin, and how thing?.stuff?.value evaluates to null if any of those variables in the chain are null
I am trying to pass an ArrayList<Profile> in a bundle from one fragment to another using the Navigation Graph, but I am getting this error Type mismatch: inferred type is Array<Profile> but Array<(out) Parcelable!>? was expected I have already passed on the navigation the type of argument that I want to pass. What am I missing? Here my code
Code that passes the argument
emptyHomeViewModel.playerByIDLiveData.observe(viewLifecycleOwner) { profile ->
if (profile != null) {
profilesList.add(profile)
bundle = Bundle().apply {
putSerializable("user", profilesList)
}
findNavController().navigate(
R.id.action_emptyHomeFragment_to_selectUserFragment,
bundle
)
Navigation XML for the fragment that will receive
<fragment
android:id="#+id/selectUserFragment"
android:name="com.example.dota2statistics.SelectUserFragment"
android:label="fragment_select_user"
tools:layout="#layout/fragment_select_user" >
<argument
android:name="user"
app:argType="com.example.dota2statistics.data.models.byID.Profile[]" />
</fragment>
Code of the fragment that receives the ArrayList
class SelectUserFragment : Fragment(R.layout.fragment_select_user) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val args : SelectUserFragmentArgs by navArgs()
val profilesList = args.user
Log.i("Profiles", "onViewCreated: ${profilesList[0].personaname} ================")
}
add this plugin
plugins {
id("kotlin-parcelize")
}
then make your class parcelable for example
import kotlinx.parcelize.Parcelize
#Parcelize
class User(val firstName: String, val lastName: String, val age: Int): Parcelable
Safe args only allows passing Array so before adding bundle we have to convert ArrayList to Array
bundle.putParcelableArray("user", profilesList.toTypedArray())
Then when getting the argument we can convert it back to ArrayList
val list: ArrayList<Profile> = ArrayList(args.user.toList())
I use MVVM and have a list of data elements in a database that is mapped through a DAO and repository to ViewModel functions.
Now, my problem is rather banal; I just want to use the data in fragment variables, but I get a type mismatch.
The MVVM introduces a bit of code, and for completeness of context I'll run through it, but I'll strip it to the essentials:
The data elements are of a data class, "Objects":
#Entity(tableName = "objects")
data class Objects(
#ColumnInfo(name = "object_name")
var objectName: String
) {
#PrimaryKey(autoGenerate = true)
var id: Int? = null
}
In ObjectsDao.kt:
#Dao
interface ObjectsDao {
#Query("SELECT * FROM objects")
fun getObjects(): LiveData<List<Objects>>
}
My database:
#Database(
entities = [Objects::class],
version = 1
)
abstract class ObjectsDatabase: RoomDatabase() {
abstract fun getObjectsDao(): ObjectsDao
companion object {
// create database
}
}
In ObjectsRepository.kt:
class ObjectsRepository (private val db: ObjectsDatabase) {
fun getObjects() = db.getObjectsDao().getObjects()
}
In ObjectsViewModel.kt:
class ObjectsViewModel(private val repository: ObjectsRepository): ViewModel() {
fun getObjects() = repository.getObjects()
}
In ObjectsFragment.kt:
class ObjectsFragment : Fragment(), KodeinAware {
private lateinit var viewModel: ObjectsViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this, factory).get(ObjectsViewModel::class.java)
// I use the objects in a recyclerview; rvObjectList
rvObjectList.layoutManager = GridLayoutManager(context, gridColumns)
val adapter = ObjectsAdapter(listOf(), viewModel)
// And I use an observer to keep the recyclerview updated
viewModel.getObjects.observe(viewLifecycleOwner, {
adapter.objects = it
adapter.notifyDataSetChanged()
})
}
}
The adapter:
class ObjectsAdapter(var objects: List<Objects>,
private val viewModel: ObjectsViewModel):
RecyclerView.Adapter<ObjectsAdapter.ObjectsViewHolder>() {
// Just a recyclerview adapter
}
Now, all the above works fine - but my problem is that I don't want to use the observer to populate the recyclerview; in the database I store some objects, but there are more objects that I don't want to store.
So, I try to do this instead (in the ObjectsFragment):
var otherObjects: List<Objects>
// ...
if (condition) {
adapter.objects = viewModel.getObjects()
} else {
adapter.objects = otherObjects
}
adapter.notifyDataSetChanged()
And, finally, my problem; I get type mismatch for the true condition assignment:
Type mismatch: inferred type is LiveData<List> but List was expected
I am unable to get my head around this. Isn't this pretty much what is happening in the observer? I know about backing properties, such as explained here, but I don't know how to do that when my data is not defined in the ViewModel.
We need something to switch data source. We pass switching data source event to viewModel.
mySwitch.setOnCheckedChangeListener { _, isChecked ->
viewModel.switchDataSource(isChecked)
}
In viewModel we handle switching data source
(To use switchMap include implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
class ObjectsViewModel(private val repository: ObjectsRepository) : ViewModel() {
// Best practice is to keep your data in viewModel. And it is useful for us in this case too.
private val otherObjects = listOf<Objects>()
private val _loadDataFromDataBase = MutableLiveData<Boolean>()
// In case your repository returns liveData of favorite list
// from dataBase replace MutableLiveData(otherObjects) with repository.getFavorite()
fun getObjects() = _loadDataFromDataBase.switchMap {
if (it) repository.getObjects() else MutableLiveData(otherObjects)
}
fun switchDataSource(fromDataBase: Boolean) {
_loadDataFromDataBase.value = fromDataBase
}
}
In activity/fragment observe getObjects()
viewModel.getObjects.observe(viewLifecycleOwner, {
adapter.objects = it
adapter.notifyDataSetChanged()
})
You can do something like this:
var displayDataFromDatabase = true // Choose whatever default fits your use-case
var databaseList = emptyList<Objects>() // List we get from database
val otherList = // The other list that you want to show
toggleSwitch.setOnCheckedChangeListener { _, isChecked ->
displayDataFromDatabase = isChecked // Or the negation of this
// Update adapter to use databaseList or otherList depending upon "isChecked"
}
viewModel.getObjects.observe(viewLifecycleOwner) { list ->
databaseList = list
if(displayDataFromDatabase)
// Update adapter to use this databaseList
}
I use LiveData in my layout file, and add observe event for some LiveData variable , you can see Code C.
1: Why can I use assign either this.viewLifecycleOwner or this to binding.lifecycleOwner in Code A?
2: I think mHomeViewModel.listVoiceBySort.observe(this) {... } in Code B can work well, but in fact, it failed, why?
Code A
binding.lifecycleOwner = this.viewLifecycleOwner //It can work well
binding.lifecycleOwner = this //It can work well
Code B
mHomeViewModel.listVoiceBySort.observe(viewLifecycleOwner) { //It can work well
mHomeViewModel.listVoiceBySort.observe(this) { //It cannot work
Code C
class FragmentHome : Fragment() {
private lateinit var binding: LayoutHomeBinding
private val mHomeViewModel by lazy {
getViewModel {
HomeViewModel(mActivity.application, provideRepository(mContext))
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(
inflater, R.layout.layout_home, container, false
)
binding.lifecycleOwner = this.viewLifecycleOwner
//binding.lifecycleOwner = this //It can work well
binding.aHomeViewModel = mHomeViewModel
mHomeViewModel.listVoiceBySort.observe(viewLifecycleOwner) {
//mHomeViewModel.listVoiceBySort.observe(this) { //It cannot work
myAdapter.submitList(it)
}
...
return binding.root
}
private fun showActionMenu() {
val view = mActivity.findViewById<View>(R.id.menuMoreAction) ?: return
PopupMenu(mContext, view).run {
menuInflater.inflate(R.menu.menu_option_action, menu)
for (item in menu){
if (item.itemId == R.id.menuMoreActionShowCheckBox){
mHomeViewModel.displayCheckBox.observe(this#FragmentHome){
//mHomeViewModel.displayCheckBox.observe(this#FragmentHome.viewLifecycleOwner){ //It can work well
if (it){
item.title =mContext.getString(R.string.menuMoreActionHideCheckBox)
}else{
item.title =mContext.getString(R.string.menuMoreActionShowCheckBox)
}
}
}
}
...
}
}
...
}
viewLifecycleOwner is available only after view of the Fragment is being inflated. Referencing the Fragment this for lifecycleOwner will use the Fragment lifecycle.
You can use this (referencing the Fragment) before view of the fragment is inflated, in onAttach() and onCreateView(). While viewLifecycle can be used in and after any point after view is completely created, after onViewCreated().
1: Why can I use assign either this.viewLifecycleOwner or this to binding.lifecycleOwner in Code A?
Because they are both LifecycleOwner. Although you generally want to use viewLifecycleOwner from onViewCreated, otherwise you can get bugs on fragments that are in "limbo state" (replace().addToBackStack()'d).
2.) private fun showActionMenu() { ... observe(
You generally shouldn't "start observing" things that are triggered by side-effects, otherwise you will end up with observers that don't unregister even after your PopupMenu is dismissed, for example.
I am trying to remove duplicating methods by creating one singular method that takes params. I have a few methods that do the exact thing where they create an instance of a class, a fragment manager and then shows the fragment. Just want to know how I can shorten the following into one method and just pass in params.
private fun openAboutDialogue() {
//get a fragment manager
val fm = fragmentManager
val abtDialogue = GetStartedFragment()
abtDialogue.show(fm, "About the App")
}
private fun openNewRouteDialogue() {
val confirmNewDialogue = NewRouteFragment()
val fm = fragmentManager
confirmNewDialogue.show(fm, "NewRoute")
}
private fun openEndRouteDialogue() {
val confirmEndDialogue = TrafficDataFragment()
val fm = fragmentManager
confirmEndDialogue.show(fm, "GetTraffic")
}
If I understand this correcly, you simply create something like the following which takes Fragment as an argument:
private fun openDialogue(fragment: Fragment, text: String) =
fragment.show(fragmentManager, text)
Technically you could do
fun AppCompatActivity.openDialogue(fragment: DialogFragment, tag: String) {
fragment.show(supportFragmentManager, tag)
}
But now you have to call it as
openDialogue(GetStartedFragment(), "About the App")
openDialogue(NewRouteFragment(), "NewRoute")
openDialogue(TrafficDataFragment(), "GetTraffic")
If you want to get fancy and hide Fragment class from the caller you can use an enum for selection which can double as a fragment tag as well:
enum class DialogueType{ GET_STARTED, NEW_ROUTE, TRAFFIC,DATA }
private fun openDialogue(type: DialogueType){
val fragment = when(type) {
GET_STARTED -> GetStartedFragment()
NEW_ROUTE -> NewRouteFragment()
TRAFFIC_DATA -> TrafficDataFragment()
}
fragment.show(fragmentManager, type.name)
}