In my app, i have a fragment that call a remote service for get user profile information and show it, and I've used DataBinding for show data.
This is my layout:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="com.myapp.ProfileViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="#+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#{viewModel.profile.firstName+ ' '+ viewModel.profile.lastName}" />
<!-- Other textviews -->
</LinearLayout>
</layout>
this is ProfileViewModel class
class ProfileViewModel : ViewModel() {
#Inject
lateinit var profileRepository: ProfileRepository
private var _profile = MutableLiveData<Profile>()
val profile: LiveData<Profile>
get() = _profile
fun getProfile(token: String) {
profileRepository.profile(
token,
{
// success
_profile.value = it.value
},
{
//error
}
)
}
}
data class Profile(
firstName : String,
lastName : String,
// other fields
)
And this is fragment where profile should be showed:
class ProfileFragment : Fragment() {
private lateinit var binding: FragmentProfileBinding
private lateinit var viewModel: ProfileViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_profile,
container,
false
)
viewModel = activity?.run {
ViewModelProviders.of(this)[ProfileViewModel::class.java]
} ?: throw Exception("Invalid Activity")
binding.viewModel = viewModel
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.getProfile(
"aToken"
)
}
}
Now it happen that first time i open fragment, repository call service and get data correctly, but "null" is showed inside textviews. If i close fragment and i reopen it, edittext are populate correctly. What's wrong with my code?
Set binding.lifecycleOwner in your fragment class in order for updates in LiveData objects to be reflected in corresponding views. Your fragment class should look like this:
class ProfileFragment : Fragment() {
private lateinit var binding: FragmentProfileBinding
private lateinit var viewModel: ProfileViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_profile,
container,
false
)
viewModel = activity?.run {
ViewModelProviders.of(this)[ProfileViewModel::class.java]
} ?: throw Exception("Invalid Activity")
binding.viewModel = viewModel
//add lifecycleOwner
binding.lifecycleOwner = this
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.getProfile(
"aToken"
)
}
}
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(" ")
}
}
}
I am stuck here, help.
I've got the following code:
ProfileFragment:
#AndroidEntryPoint
class ProfileFragment : Fragment() {
private val profileViewModel: ProfileViewModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = FragmentProfileBinding.inflate(inflater, container, false)
binding.viewModel = profileViewModel
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
}
ProfileViewModel:
class ProfileViewModel #ViewModelInject constructor(
#ApplicationContext private val context: Context,
private val profileRepository: ProfileRepository
) : ViewModel() {
fun getUser() {
....
}
}
fragment_profile.xml:
<data>
<variable
name="viewModel"
type="my.app.viewmodel.ProfileViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="match_parent"
android:layout_height="55dp"
android:onClick="#{()->viewModel.getUser()}" />
</LinearLayout>
The problem is that the onClick is never triggered no matter what I do and how I try.
However, as soon as I do it like this in ProfileFragment it works just fine:
binding.myButton.setOnClickListener {
profileViewModel.getUser()
}
Any ideas? Cause I am stuck here
I am not sure if this issue still exist or not
but if you are using data binding and hilt for dependency injection please add below lines in your fragment onViewCreated
binding.lifecycleOwner = this
binding.viewModel = profileViewModel
What fixed this problem for me was adding the fragment reference to itself in onCreateView:
binding.<fragment_name> = this
Like so:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Log.v(TAG, "onCreateView")
binding = FragmentSettingsBinding.inflate(inflater, container, false)
binding.settingsFragment = this <---
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
I have a fragment ProductsFragment in which I have a button AddProduct when it is clicked I want to open a different fragment AddProductFragment.
I am using MVVM architecture
I went through this link and done the below mentioned implementation, but I did not quite understand or did not mention where fragment I want to navigate to
Error message
ProductsFragment - THE ISSUE IS HERE IN ONVIEWCREATED METHOD*
class ProductsFragment: Fragment() {
private lateinit var binding: ProductsBinding
private lateinit var navController: NavController
private lateinit var productsViewModel: ProductsViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.products, container, false)
val dao = SubscriberDatabase.getInstance(activity!!.applicationContext).productDAO
val repository = ProductRepository(dao)
val factory = ProductsViewModelFactory(repository, activity!!.applicationContext)
productsViewModel = ViewModelProvider(this, factory).get(ProductsViewModel::class.java)
binding.productsViewModel = productsViewModel
binding.lifecycleOwner = this
val view = binding.root
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navController = Navigation.findNavController(view)
productsViewModel.navigateScreen.observe(activity!!, EventObserver {
navController.navigate(it) //issues is here
})
}
}
Products
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android">
<data class=".ProductsBinding">
<variable
name="productsViewModel"
type="com.rao.iremind.ProductsViewModel" />
</data>
<LinearLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Testing text"/>
<Button
android:id="#+id/btn_add_product"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add product"
android:onClick="#{() -> productsViewModel.addProduct()}"/>
<View
android:id="#+id/frgSpace"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
</layout>
ProductViewModel
class ProductsViewModel (
private val repository: ProductRepository,
private val context: Context
): ViewModel() {
private val _navigateScreen = MutableLiveData<Event<Any>>()
val navigateScreen: LiveData<Event<Any>> = _navigateScreen
fun addProduct() {
Toast.makeText(context, "Products view model", Toast.LENGTH_LONG).show()
_navigateScreen.value = Event(R.id.frgSpace)
}
}
Event
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
class EventObserver<Int>(private val onEventUnhandledContent: (Int) -> Unit) : Observer<Event<Int>> {
override fun onChanged(event: Event<Int>?) {
event?.getContentIfNotHandled()?.let {
onEventUnhandledContent(it)
}
}
}
ProductsViewModelFactory
class ProductsViewModelFactory (
private val repository: ProductRepository,
private val context: Context
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ProductsViewModel::class.java)) {
return ProductsViewModel(repository, context) as T
}
throw IllegalArgumentException("Unknown View Model class")
}
}
I want to navigate to this fragment
class AddProductFragment: Fragment() {
private lateinit var binding: AddProductBinding
private lateinit var addProductViewModel: AddProductViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.add_product, container, false)
val dao = SubscriberDatabase.getInstance(activity!!.applicationContext).productDAO
val repository = ProductRepository(dao)
val factory = ProductsViewModelFactory(repository, activity!!.applicationContext)
addProductViewModel = ViewModelProvider(this, factory).get(AddProductViewModel::class.java)
binding.addProductViewModel = addProductViewModel
binding.lifecycleOwner = this
val view = binding.root
return view
}
}
Thanks
R
It seems that your EventObserver class is expecting an Int but you are sending Any in LiveData<Event<Any>>
Try changing
private val _navigateScreen = MutableLiveData<Event<Any>>()
val navigateScreen: LiveData<Event<Any>> = _navigateScreen
to
private val _navigateScreen = MutableLiveData<Event<Int>>()
val navigateScreen: LiveData<Event<Int>> = _navigateScreen
I would also recommend you to replace activity!! with viewLifecycleOwner in this line:
productsViewModel.navigateScreen.observe(viewLifecycleOwner, EventObserver {...})
so that your fragment does not receive any LiveData updates when its view is destroyed.
I create a data binding for my fragment.
// fragment_my
<layout ...>
<data>
<variable
name="myData"
type="com.example.myproject.MyData" />
</data>
...
</layout>
// MyFragment
class MyFragment : Fragment() {
private lateinit var binding: FragmentMyBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater, R.layout.fragment_my, container, false
)
Log.d(TAG, ${binding.myData.item})
return binding.root
}
}
// MyData
data class MyData(
var item: String = "123"
)
But when I run my project, it show the item not init correctly and it always return an optional nullable value.
You should bind the data before accessing/using it.
Try this:
binding = DataBindingUtil.inflate(
inflater, R.layout.fragment_my, container, false
)
binding.myData = MyData()
Log.d(TAG, ${binding.myData.item})
return binding.root
binding.myData = MyData() is missing in your code.
Source: https://developer.android.com/topic/libraries/data-binding/expressions#binding_data
im trying to make a base bottom sheet dialog fragment class that supports data binding. here is my class:
abstract class RoundedBottomSheetDialogFragment<VM : BaseViewModel, DB : ViewDataBinding> :
BottomSheetDialogFragment() {
abstract val viewModel: VM
open lateinit var binding: DB
private fun init(inflater: LayoutInflater, container: ViewGroup?) {
binding = DataBindingUtil.inflate(inflater, getLayoutRes(), container, true)
}
abstract fun getLayoutRes(): Int
abstract fun configEvents()
abstract fun bindObservables()
/**
*
* You need override this method.
* And you need to set viewModel to binding: binding.viewModel = viewModel
*
*/
abstract fun initBinding()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val parentLayout = inflater.inflate(R.layout.rounded_bottom_sheet, container, false)
init(inflater, parentLayout.container)
showDialogAsExpanded()
return parentLayout
}
private fun showDialogAsExpanded() {
dialog?.setOnShowListener {
val bottomSheetInternal =
(dialog as BottomSheetDialog).findViewById<View>(R.id.design_bottom_sheet) ?: return#setOnShowListener
val behavior = BottomSheetBehavior.from(bottomSheetInternal)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.skipCollapsed = true
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
configEvents()
bindObservables()
}
}
if you see i'm inflating a layout inside this dialog fragment class and i want to use data binding inside that layout xml file.
this is an example of my xml file:
<layout>
<data>
<variable
name="vm"
type="com.mobtakerteam.walleto.ui.login.searchcountry.SearchCountryViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/search_list"
android:layout_width="match_parent"
android:layout_height="400dp"
app:data="#{vm.countries}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="20"
tools:listitem="#layout/fragment_search_country_row" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
but the problem is that it is not working in the xml layout and i have to manually observe the live data objects inside kotlin class like this:
viewModel.countries.observe(this, Observer {
adapter.submitList(it)
})
so what is the problem?
Using LiveData with binding you have to set lifecycle owner like this
binding.lifecycleOwner = this