I want use one viewModel and observe object from one method. But I dont want write this observe method in all fragment. Only write in one place and use other fragments. I think I need fragment extension but can't get it how do this. I need help.
This viewModel that I want use.
SharedViewModel.kt
class SharedViewModel #Inject constructor(private val notificationServiceRepo: NotificationServiceRepo) : ViewModel() {
private val _helpNotification = SingleLiveEvent<NetworkResult<BaseResponse<Any>>>()
val helpNotification get() = _helpNotification
fun postHelpNotification(helpNotificationRequest: HelpNotificationRequest) = viewModelScope.launch (
Dispatchers.IO){
_helpNotification.postValue(NetworkResult.Loading)
_helpNotification.postValue(notificationServiceRepo.postNotificationHelp(helpNotificationRequest))
}
}
this is call method and observe function:
MainFragment.kt
viewModel.postHelpNotification(HelpNotificationRequest(0))
viewModel.helpNotification.observe(viewLifecycleOwner) {
it?.onLoading {}
it?.onSuccess { result->
result?.let {
InfoDialogWithOneText(
InfoDialogType.GOT_HELP_ASKING_FROM_STAFF
).show(childFragmentManager, InfoDialog.TAG)
}
}
it?.onError { error ->
InfoDialogWithOneText(
InfoDialogType.GOT_HELP_ASKING_FROM_STAFF
).show(childFragmentManager, InfoDialog.TAG)
}
}
}
Tried use sharedViewModel but I will have to write observe method for all.
Tried to baseViewModel but it get error hilt view model and also it will be same like shared view model.
For the abstract part, you want it need to cover everything - from having a view model that provides the observable event and handling it. I had to change some types because you did not provide them but it should not be too difficult to apply this to your case.
abstract class HelpNotificationFragment : Fragment() {
internal abstract val viewModel: HelpNotificationViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.helpNotification.observe(viewLifecycleOwner) { result ->
println(result)
}
}
}
abstract class HelpNotificationViewModel : ViewModel() {
private val _helpNotification = SingleLiveEvent<Result<Any>>()
// Specify the immutable type otherwise you would expose it as mutable
val helpNotification: LiveData<Result<Any>>
get() = _helpNotification
fun postHelpNotification(helpNotificationRequest: Result<Any>) {
viewModelScope.launch(Dispatchers.IO) {
_helpNotification.postValue(helpNotificationRequest)
}
}
}
And this is how you would implement the fragments that would use it - the overriding the view model will take care of forcing you to use view model with proper parent:
#HiltViewModel
class MainViewModel #Inject constructor(): HelpNotificationViewModel()
#AndroidEntryPoint
class MainFragment : HelpNotificationFragment() {
override val viewModel: MainViewModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_main, container, false)
view.findViewById<Button>(R.id.button).setOnClickListener {
viewModel.postHelpNotification(Result.success("Yay!"))
}
return view
}
}
Related
I am currently reading data from a Bluetooth Sensor, hence the data changes in real-time and continuously changes. I have stored the data in a variable: liveData:ByteArray
Now I am trying to send liveData from MainActivity to Sensordisplayfragment.
UPDATE
Based on #CTD's comment, this is what I have tried, unfortunately I do not have much knowledge on viewModel, and online research is just confusing as there seems to be many methods to implement a viewModel.
In my MainActivity class where variable liveData is stored:
val model:MyViewModel by viewModels()
private fun processLiveData(liveData : ByteArray){
livedata = liveData
model.uploadData(livedata)
}
In MyViewModel.class where the viewModel is at:
class MyViewModel: ViewModel() {
private val realtimedata = MutableLiveData<ByteArray>()
fun uploadData(data:ByteArray){
realtimedata.value = data
}
fun loadData():LiveData<ByteArray>{
return realtimedata
}
}
Finally, in my Sensordisplay fragment where I am fetching the data:
val model:MyViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
model.loadData().observe(viewLifecycleOwner,Observer<ByteArray>{
passandprocessLiveData(it)
})
return inflater.inflate(R.layout.sensordisplay, container, false)
}
override fun onResume(){
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
model.loadData().observe(viewLifecycleOwner,Observer<ByteArray>{
passandprocessLiveData(it)
})
super.onResume()
}
fun passandprocessLiveData(data:Bytearray){
//extract information from data and make
//cardviews move in realtime according to the extracted data
}
Unfortunately,nothing is getting transferred and my cardviews are not moving. I can guarantee there is no error in the moving of the cardview codes. Anyone able to advice on what I can add? Apparently there is an init() function that I need to use.
class MyViewModel : ViewModel() {
private val realtimedata = MutableLiveData<ByteArray>()
val sensorData: LiveData<ByteArray> = realtimedata
fun update(data: ByteArray){
realtimedata.value = data
}
}
class MainActivity: Activity() {
private val viewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bluetoothSensorCallBack { data ->
// Update the realtimedata
viewModel.update(data)
}
}
}
class SensordisplayFragment : Fragment() {
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: MyViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.sensorData.observe(viewLifecycleOwner, Observer<ByteArray> { data ->
// Update the UI
})
}
}
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 am trying out flows and trying to see how they can be converted to mvvm with android view models. Here is what I tried first to test it out :
class HomeViewModel : ViewModel() {
private lateinit var glucoseFlow: LiveData<Int>
var _glucoseFlow = MutableLiveData<Int>()
fun getGlucoseFlow() {
glucoseFlow = flowOf(1,2).asLiveData()
_glucoseFlow.value = glucoseFlow.value
}
}
class HomeFragment : Fragment() {
private lateinit var viewModel: HomeViewModel
override fun onCreateView (
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.home_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(HomeViewModel::class.java)
viewModel._glucoseFlow.observe(this, Observer {
handleUpdate(it)
})
viewModel.getGlucoseFlow()
}
private fun handleUpdate(reading : Int) {
glucose_reading.text = reading.toString()
}
}
I get a null for the reading number however any ideas ?
This happens because you are trying to assign glucoseFlow.value to _glucoseFlow.value directly, I guess you should use a MediatorLiveData<Int>, however this is not my final suggestion.
You can solve it if you collect flow items and then assign them to your private variable.
// For private variables, prefer use underscore prefix, as well MutableLiveData for assignable values.
private val _glucoseFlow = MutableLiveData<Int>()
// For public variables, prefer use LiveData just to read values.
val glucoseFlow: LiveData<Int> get() = _glucoseFlow
fun getGlucoseFlow() {
viewModelScope.launch {
flowOf(1, 2)
.collect {
_glucoseFlow.value = it
}
}
}
Having the before implementation over the HomeViewModel, start to observe your public glucoseFlow from HomeFragment and you will be able to receive non-null sequence values (1 and then 2).
If you are using databinding, do not forget specify the fragment view as the lifecycle owner of the binding so that the binding can observe LiveData updates.
class HomeFragment : Fragment() {
...
binding.lifecycleOwner = viewLifecycleOwner
}
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()
}
Am Using Koin as Dependency injection pattern in my project, I need to create new instances whenever i load fragment/activity, now am using the following pattern, Any solution for that it might save lots of time.
private val homeViewModel: HomeViewModel by viewModel()
The question is why you want new instances everytime? The whole concept of ViewModel is to retain the same instance and data. viewModel {} creates new instance everytime you inject it unless it is not shared.
Don't know why it is not working for you, but I think you can use factory{} instead of viewModel{}.
factory{
// this is because you need new instance everytime.
HomeViewModel()
}
Define ViewModel as an abstract in BaseFragment class and set value when you extend your BaseFragment.
abstract class BaseFragment<Binding : ViewDataBinding, ViewModel : BaseViewModel> : Fragment(){
protected var bindingObject: Binding? = null
protected abstract val mViewModel: ViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
bindingObject = DataBindingUtil.inflate(inflater, getLayoutResId(), container, false)
return bindingObject?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
performDataBinding()
}
#LayoutRes
abstract fun getLayoutResId(): Int
private fun performDataBinding() {
bindingObject?.setLifecycleOwner(this)
bindingObject?.setVariable(BR.viewModel, mViewModel)
bindingObject?.executePendingBindings()
}
}
And in your fragment
class FragmentNew : BaseFragment<FragmentNewBinding, FragmentNewVM>() {
// Here is the your viewmodel imlementation. Thus when you create fragment it's by default override method
override val mViewModel: FragmentNewVM by viewModel()
override fun getLayoutResId(): Int = [fragment layout id like "R.layout.fragment_new"]
}
You are going to want to forego using by viewmodel and instantiate the class directly. You can get global (scoped) variables through getKoin().get().
private val viewModel = HomeViewModel(getKoin().get())