Is there a more generic way to init this two initialization lines?
private var _binding: MyFragmentViewBinding? = null
private val binding get() = _binding!!
Should we call every time binding
binding.cancelButton.setOnClickListener { }
binding.homeButton.setOnClickListener { }
binding.aboutButton.setOnClickListener { }
Or to create class variable?
cancelButton = binding.cancelButton
binding.cancelButton.setOnClickListener{}
And, should we set binding = null in adapter?
I think it is more of a personal preference. I like to do it with extension and higher-order function
fun <T : ViewDataBinding> Fragment.getDataBinding(layout: Int, container: ViewGroup?): T {
val binding: T = DataBindingUtil.inflate(layoutInflater, layout, container, false)
binding.lifecycleOwner = viewLifecycleOwner
return binding
}
My Fragment looks like this.
class InviteFragment : Fragment() {
private lateinit var binding: FragmentInviteBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = getDataBinding(R.layout.fragment_invite, container)
return binding.apply {
inviteAll.setOnClickListener(onInviteAllClick)
// You can set as many click listener here
// or some initialization related to view such as
// setting up recycler view adapter and decorators
}.root
}
private val onInviteAllClick = View.OnClickListener {
// Invite users
}
}
By doing things like this your onCreateView will be more readable and never going to get very long.
Related
I have two fragments that share information with each other, in the first one I have an edit text and button widget. The second fragment is just a listview. When the user clicks the button, it displays whatever is in the edit text widget in the second fragment.
So if the user enters the text study and clicks the button the second fragment will display
Study
If the user then enters the text eat and clicks the button, the second fragment will display
Study
Eat
I am having so issues with displaying the texts
So far this is what I have done
class FirstFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
viewModel = activity?.run { ViewModelProvider(this)[MyViewModel::class.java]
} ?: throw Exception("Invalid Activity")
val view = inflater.inflate(R.layout.one_fragment, container, false)
val button = view.findViewById<Button>(R.id.vbutton)
val value = view.findViewById<EditText>(R.id.textView)
button.setOnClickListener {
}
return view;
}
}
class SecondFragment : Fragment() {
lateinit var viewModel: MyViewModel
#SuppressLint("MissingInflatedId")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
viewModel = activity?.run { ViewModelProvider(this)[MyViewModel::class.java]
} ?: throw Exception("Invalid Activity")
val view = inflater.inflate(R.layout.page3_fragment, container, false)
val valueView = v.findViewById<TextView>(R.id.textView)
return view
The problem I am having is how to display the texts
If I undestand you correctly, you want to share data between fragments? If yes, you can do that with "shared" viewModel. For example:
class FirstFragment : Fragment() {
private var _binding: FragmentFirstBinding? = null
private val binding get() = _binding!!
private val sharedViewModel by activityViewModels<SharedViewModel>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentFirstBinding.inflate(inflater, container, false)
binding.buttonChangeFragment.setOnClickListener {
/*
You can change data here, or in navigateWithNavController() from
activity (You already have an instance of your viewModel in activity)
*/
sharedViewModel.changeData(binding.myEditText.text.toString())
if (requireActivity() is YourActivity)
(requireActivity() as YourActivity).navigateWithNavController()
}
return binding.root
}
}
class SecondFragment : Fragment() {
private var _binding: FragmentSecondBinding? = null
private val binding get() = _binding!!
private val sharedViewModel by activityViewModels<SharedViewModel>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSecondBinding.inflate(inflater, container, false)
binding.secondFragmentText.text = sharedViewModel.someData.value
return binding.root
}
}
and your activity:
class YourActivity: AppCompatActivity() {
private lateinit var binding: YourActivityBinding
private lateinit var appBarConfiguration: AppBarConfiguration
private val sharedViewModel: SharedViewModel by lazy {
ViewModelProvider(
this
)[SharedViewModel::class.java]
}
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = YourActivityBinding.inflate(LayoutInflater.from(this))
setContentView(binding.root)
navController = this.findNavController(R.id.nav_host_fragment)
appBarConfiguration = AppBarConfiguration(navController.graph)
}
/*
This function is just for test
*/
fun navigateWithNavController() {
navController.navigate(R.id.secondFragment)
}
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController, appBarConfiguration)
}
}
And your viewModel should look something like this:
class SharedViewModel : ViewModel() {
private val _someData = MutableLiveData("")
val someData: LiveData<String>
get() = _someData
fun changeData(newData: String?) {
_someData.value = newData ?: _someData.value
}
}
Your view model should have a backing list of the entered words. When a word is added, the list can be updated, and in turn you can update a LiveData that publishes the latest version of the list.
class MyViewModel: ViewModel() {
private val backingEntryList = mutableListOf<String>()
private val _entryListLiveData = MutableLiveData("")
val entryListLiveData : LiveData<String> get() = _entryListLiveData
fun addEntry(word: String) {
backingEntryList += word
_entryListLiveData.value = backingEntryList.toList() // use toList() to to get a safe copy
}
}
Your way of creating the shared view model is the hard way. The easy way is by using by activityViewModels().
I also suggest using the Fragment constructor that takes a layout argument, and then setting things up in onViewCreated instead of onCreateView. It's less boilerplate code to accomplish the same thing.
In the first fragment, you can add words when the button's clicked:
class FirstFragment : Fragment(R.layout.one_fragment) {
private val viewModel by activityViewModels<MyViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val button = view.findViewById<Button>(R.id.vbutton)
val value = view.findViewById<EditText>(R.id.textView)
button.setOnClickListener {
viewModel.addEntry(value.text.toString())
}
}
}
In the second fragment, you observe the live data:
class SecondFragment : Fragment(R.layout.page3_fragment) {
private val viewModel by activityViewModels<MyViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val valueView = view.findViewById<TextView>(R.id.textView)
viewModel.entryListLiveData.observe(viewLifecycleOwner) { entryList ->
valueView.text = entryList.joinToString(" ")
}
}
}
My app should be able to change color of one part of my screen in a Fragment (called Color Preview) from Color Picker that is DialogFragment and appears once a Button is clicked. I used the same ViewModel with MutableLiveData in it within Fragment and DialogFragment. However I am not able to get Data about color in Fragment when I pick some color in DialogFragment. Code below.
Dialog Fragment:
class ColorFragment : DialogFragment() {
private var _binding: FragmentColorBinding? = null
private val binding get() = _binding!!
private val viewModel: MainViewModel by lazy {
getViewModel {
MainViewModel()
}
}
#SuppressLint("DialogFragmentInsteadOfSimpleDialog")
override fun onStart() {
super.onStart()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
FragmentColorBinding.inflate(inflater, container, false).apply {
_binding = this
lifecycleOwner = this#ColorFragment
mainViewModel = getViewModel()
return root
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.color02.setOnClickListener {
viewModel.currentLightProfileColor.value = "#00ffff"
}
}
Main Fragment (where color should be shown) looks like this:
class MainFragment : Fragment() {
private var _binding: FragmentMainBinding? = null
private val binding get() = _binding!!
private var viewCreated = false
private val viewModel: MainViewModel by lazy {
getViewModel {
MainViewModel()
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
FragmentMainBinding.inflate(inflater, container, false).apply {
_binding = this
lifecycleOwner = this#MainFragment
mainViewModel = getViewModel()
executePendingBindings()
viewCreated = true
return root
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.colorPreview.holder?.addCallback(this)
viewModel.currentLightProfileColor.observe(viewLifecycleOwner) { color ->
Color.parseColor(color.toString()).let { color ->
Timber.d("color LiveDataGet: ${color}")
binding.colorPreview.setBackgroundColor(color)
}
}
binding.color.setOnClickListener {
findNavController().navigate(R.id.ColorFragment)
}
}
In my ViewModel there is only MutableLiveData for color:
class MainViewModel : ViewModel() {
val currentLightProfileColor = MutableLiveData<String>()}
I dont get any errors but once I click on color2 I should get from Timber in Log: "color LiveDataGet: #00ffff" but I dont and color preview does not change color.
Did I miss something?
I would appreciate if someone could quickly take a look at my code. Thanks!
I found a solution. In Kotlin, data is not shared if viewModels() is instantiated without activityViewModels().
So I changed my code, instead of:
private val viewModel: MainViewModel by lazy {
getViewModel {
LightmvpViewModel()
}
}
I wrote:
private val viewModel: MainViewModel by activityViewModels()
With that simple change, everything should work.
I implemented a simple recyclerview using databinding using kotlin language.
In xml, recyclerview seems to be the default,
but when I run it, it doesn't show up as default.
Other buttons and views included in the fragment appear normally when executed, but only the recyclerview is not visible. Which part is wrong?
class WorkFragment : Fragment() {
private var _binding: FragmentWorkBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentWorkBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val data = mutableListOf<WorkList>()
val adapter = WorkAdapter()
adapter.data = data
binding.recycler1.adapter = adapter
binding.recycler1.layoutManager = LinearLayoutManager(requireContext())
}
}
class WorkAdapter : RecyclerView.Adapter<WorkAdapter.ViewHolder>() {
var data = mutableListOf<WorkList>()
class ViewHolder(val binding: RecyclerviewWorkItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(workList: WorkList) {
binding.tvStarttime.text = workList.Starttime
binding.tvAdmin.text = workList.Admin
binding.tvPart.text = workList.Part
binding.tvStoptime.text = workList.Stoptime
binding.tvWorkers.text = workList.Workers.toString()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WorkAdapter.ViewHolder {
val binding = RecyclerviewWorkItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: WorkAdapter.ViewHolder, position: Int) {
holder.bind(data[position])
}
override fun getItemCount(): Int {
return data.size
}
// fun replaceList(newList: MutableList<WorkList>) {
// data = newList.toMutableList()
// notifyDataSetChanged()
// }
}
Is there something wrong with my code?
For reference, I just used a fragment, not an activity.
its typically for the RecyclerView to behave like its hidden but actually its not. the reason is you are passing an empty list so the recycler will not be extended or show because there is no items in the list.
I have a parent fragment which fetches a list from API using ViewModel and Retrofit, the ViewModel is injected with Hilt.
After the list gets fetched the parent fragment will pass to its child fragment that is inside of parent fragment.
but the problem is that ViewModel is instantiated one more time in the child fragment.
Parent Fragment
#AndroidEntryPoint
class ParentFragment : Fragment() {
override val mViewModel: URLViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mViewBinding = getViewBinding(inflater, container)
mViewModel.liveData.observe(this, { data ->
{
childFragmentManager.beginTransaction().apply {
replace(
mViewBinding.fragmentContainer.id,
ChildFragment(data)
)
}
commit()
} })
mViewModel.getURL("TEST", "2021-06-18", "2021-07-18", 1 , 0 , -1, false)
return mViewBinding.root
}
}
ChildFragment
#AndroidEntryPoint
class ChildFragment(val data: List<Item>) : Fragment() {
override val mViewModel: URLViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mViewBinding = getViewBinding(inflater, container)
// mViewModel is instantiated again and some all strings properties of it is null.
return mViewBinding.root
}
}
URLViewModel
#HiltViewModel
class URLViewModel #Inject constructor(private val urlApi: URLApi): ViewModel() {
private val _urlLiveData = MutableLiveData<State<Any?>>()
val urlLiveData: LiveData<State<Any?>> = _urlLiveData
var urlName: String? = null
var beginDate: String? = null
var endDate: String? = null
var adultCount = 0
var childrenCount = 0
var airportId = 0
var isRoundTrip = false
init {
Log.e("URLViewModel", "iniialed again" )
}
#ExperimentalStdlibApi
fun getUrl(urlName: String, beginDate: String, endDate: String, adultCount: Int, childCount: Int, airportId: Int, isRoundTrip: Boolean){
Log.e("XXXXXX", "getUrl: called with url of " + urlName )
this.urlName = urlName
this.beginDate = beginDate
this.endDate = endDate
this.adultCount = adultCount
this.childrenCount = childCount
this.airportId = airportId
this.isRoundTrip = isRoundTrip
val mutableLiveData = MutableLiveData<State<Any?>>()
mutableLiveData.value = State.loading()
viewModelScope.launch {
val res = urlApi.getURL(urlName,beginDate,endDate,adultCount,childCount,airportId,isRoundTrip)
Log.e("URLVIewModel", "getUrl: response received" )
_urlLiveData.value = res
}
}
}
when I wanna access some properties like beginDate, they are null, because the ViewModel is instantiated again,
viewModels() delegation create view model against the same instance i.e Fragment's instance in your case. What you need to do is to create a shared View model .
There is helper delegate available for it with ktx libraries.
add the ktx dependency which you already have i guess from here.
implementation "androidx.fragment:fragment-ktx:1.3.4"
And create view model with
private val viewModel by activityViewModels<UrlViewModel>()
You do not have to use activity shared view model. Simply request view model from parent fragment in ChildFragment.
private val viewModel by viewModels<UrlViewModel>(ownerProducer = { requireParentFragment() })
You are trying to use share viewmodel. Try to following code for reference.
#AndroidEntryPoint
class ParentFragment : Fragment() {
private lateinit var viewModel: URLViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(requireActivity()).get(URLViewModel::class.java)
}
}
#AndroidEntryPoint
class ChildFragment : Fragment() {
private lateinit var viewModel: URLViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(requireActivity()).get(URLViewModel::class.java)
}
}
My recyclerview doesn't udpate with the livedata when I remove an
item.
The function jokeisclicked() opens up a dialog for the user which
can choose to edit or delete an item of the room database.
The delete completes, but only when I refresh the tab. How can I complete the delete without refreshing the tab?
class DashboardFragment : Fragment() {
private lateinit var dashboardViewModel: DashboardViewModel
lateinit var binding: FragmentDashboardBinding
lateinit var adapter: JokeRecyclerViewAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
//init repo and viewmodel
val dao: JokeDao = RoomDB.getInstance(requireContext()).JokeDAO
val jokerepo = JokeRepo(dao, RetrofitBuilder.jokeservice)
val factory = DashboardViewModelFactory(jokerepo)
dashboardViewModel = ViewModelProvider(this, factory).get(DashboardViewModel::class.java)
//init binding
binding = FragmentDashboardBinding.inflate(inflater, container, false)
binding.lifecycleOwner = this
//recycler observe from livedata
dashboardViewModel.jokes.observe(viewLifecycleOwner, {
binding.recyclerview.layoutManager = LinearLayoutManager(this.requireContext())
adapter = JokeRecyclerViewAdapter(it, { selected: Joke -> jokeIsClicked(selected) })
binding.recyclerview.adapter = adapter
})
return binding.root
}
fun jokeIsClicked(joke: Joke) {
//show pop up dialog
val dialog = PopUpFragment(joke)
getFragmentManager()?.let { dialog.show(it, "popUpDialog") }
}
}
here is the popupfragment class
class PopUpFragment(private val selectedJoke : Joke) : DialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
//init viewmodel
val dao = RoomDB.getInstance(this.requireContext()).JokeDAO
val jokerepo = JokeRepo(dao,RetrofitBuilder.jokeservice)
val factory = PopUpViewModelFactory(jokerepo)
val popupViewModel : PopUpFragmentViewModel = ViewModelProvider(this,factory).get(PopUpFragmentViewModel::class.java)
//init binding
val binding = FragmentPopUpBinding.inflate(inflater,container,false)
//edit clicked
binding.editjoke.setOnClickListener{
}
//delete clicked
binding.deletejoke.setOnClickListener {
//TODO
popupViewModel.deleteJoke(selectedJoke)
this.dismiss()
}
return binding.root
}
}
I think you dont update value of livedata at viewModel. Could you share viewmodles also ? If dialogViewModel removes joke from database than you have to return at #Dao's method LiveData or Flow.
Any way don't set adapert and layout manager at observe method - it's point less. For updating list at adaper you could create method and call in it notifiDataSetChanged() or (the best way of handling changes at adaper) diffUtils.
You'll need to do 2 things:
1 - Remove the data from your variable i.e if you're storying it within a an array then use array = ArrayUtils.remove(array, index);
2 - run .notifyDataSetChanged() on your RecyclerView's Adapter in order refresh the view based on the values of the new data set.