Can pass data from View to ViewModel? - android

Is there mistake if I pass data from View to ViewModel? For example, pass url from onPageFinished event of WebView. I am confused because all source tell that ViewModel mustn't have any link to View. In this case will be such link or not? Or if type of argument will be custom data class than just string?
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.webView.settings.javaScriptEnabled = true
binding.webView.webViewClient = object : WebViewClient(){
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
viewModel.onPageFinished(url) // this line
}
}
viewModel.url.observe(this) { url ->
binding.webView.loadUrl(url)
}
}
}
class MainViewModel: ViewModel() {
private val _cookieManager: CookieManager
private lateinit var _url: MutableLiveData<String>
val url: LiveData<String> = _url
init {
_url.value = "google.com"
_cookieManager = CookieManager.getInstance()
}
fun onPageFinished(url: String) {
val cookies = _cookieManager.getCookie(url)
Log.i("MainViewMovel", url)
Log.i("MainViewMovel", cookies)
}
}

The View is the UI, it has to pass some data to the View Model, like button presses, typed search queries, requests for data when the user scrolls etc. It's absolutely a two-way thing - here's the docs talking about the recommended way of designing your app. There's more on the side, including a section on the UI Layer (the View)
The terminology might be a little confusing here, because the View in Model-View-ViewModel is referring to your UI layer. But a View in Android is a layout component, which is generally tied to a specific Fragment or Activity which have shorter lifetimes than a ViewModel (one of the main points of a VM is to retain data even when Activity and Fragment instances are destroyed and recreated).
So for that reason, your ViewModel shouldn't hold any references to any Views. That's why you expose data through things like LiveData which are observed by passing a LifecycleOwner - the observers are automatically removed when they're destroyed, so the LiveData in the VM isn't keeping them alive
As far as your question goes, I don't think it hugely matters - your WebViewClient is a little bit of wiring between the View and the ViewModel, and there's always a bit of that! But if you wanted, you could always put the WebViewClient in the ViewModel, there's nothing obviously tying it to a particular view
I think that makes more sense in general - if you look at the other callbacks in WebViewClient, a lot of them are about program logic, handling events and situations. The UI layer shouldn't really be concerned with any of that, so it makes more sense to me to have the VM take care of that, decide what needs to happen, and just push any resulting UI state updates to the UI.
An alternative to keeping a singleton WebViewClient in the VM would be to have a function in there that creates a new one and configures it. That way you don't need to worry about it having any state relating to the old WebView, but all the code is still internal to the ViewModel:
class MainViewModel: ViewModel() {
...
fun getWebViewClient() = object : WebViewClient(){
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
onPageFinished(url) // calling the internal VM method
}
}
}
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
binding.webView.webViewClient = viewModel.getWebViewClient()

Related

How to prevent data duplication caused by LiveData observation in Fragment?

I'm subscribed to an observable in my Fragment, the observable listens for some user input from three different sources.
The main issue is that once I navigate to another Fragment and return to the one with the subscription, the data is duplicated as the observable is handled twice.
What is the correct way to handle a situation like this?
I've migrated my application to a Single-Activity and before it, the subscription was made in the activity without any problem.
Here is my Fragment code:
#AndroidEntryPoint
class ProductsFragment : Fragment() {
#Inject
lateinit var sharedPreferences: SharedPreferences
private var _binding: FragmentProductsBinding? = null
private val binding get() = _binding!!
private val viewModel: ProductsViewModel by viewModels()
private val scanner: CodeReaderViewModel by activityViewModels()
private fun observeBarcode() {
scanner.barcode.observe(viewLifecycleOwner) { barcode ->
if (barcode.isNotEmpty()) {
if (binding.searchView.isIconified) {
addProduct(barcode) // here if the fragment is resumed from a backstack the data is duplicated.
}
if (!binding.searchView.isIconified) {
binding.searchView.setQuery(barcode, true)
}
}
}
}
private fun addProduct(barcode: String) {
if (barcode.isEmpty()) {
return
}
viewModel.insert(barcode)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.start(args.documentId)
if (args.documentType == "Etichette") {
binding.cvLabels.visibility = View.VISIBLE
}
initUI()
observe()
}
private fun observe() {
observeBarcode()
observeProducts()
observeLoading()
observeLast()
}
}
Unfortunately, LiveData is a terribly bad idea (the way it was designed), Google insisted till they kinda phased it out (but not really since it's still there) that "it's just a value holder"...
Anyway... not to rant too much, the solution you have to use can be:
Use The "SingleLiveEvent" (method is officially "deprecated now" but... you can read more about it here).
Follow the "official guidelines" and use a Flow instead, as described in the official guideline for handling UI Events.
Update: Using StateFlow
The way to collect the flow is, for e.g. in a Fragment:
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { // or RESUMED
viewModel.yourFlow.collectLatest { ... } // or collect { ... }
}
}
For that in your ViewModel you'd expose something like:
Warning: Pseudo-Code
// Imagine your state is represented in this sealed class
sealed class State {
object Idle: State
object Loading: State
data class Success(val name: String): State
data class Failure(val reason: String): State
}
// You need an initial state
private val _yourFlow = MutableStateFlow(State.Idle)
val yourFlow: StateFlow<State> = _yourFlow
Then you can emit using
_yourFlow.emit(State.Loading)
Every time you call
scanner.barcode.observe(viewLifecycleOwner){
}
You are creating a new anonymous observer. So every new call to observe will add another observer that will get onChanged callbacks. You could move this observer out to be a property. With this solution observe won't register new observers.
Try
class property
val observer = Observer<String> { onChanged() }
inside your method
scanner.barcode.observe(viewLifecycleOwner, observer)
Alternatively you could keep your observe code as is but move it to a Fragment's callback that only gets called once fex. onCreate(). onCreate gets called only once per fragment instance whereas onViewCreated gets called every time the fragment's view is created.

Properly initialize heavy objects

I am trying to convert some Java code to Kotlin. I have a "heavy" object that I cannot reason about how to initialize properly in the app. The object can take some time to create and I don't want to block except for when the functionality is actually required. I wrote some code that meets my requirements, but it doesn't seem like a good pattern and I was hoping someone tell me what the proper pattern here (will list what I don't like about it after the code):
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.*
import javax.inject.Provider
import kotlinx.coroutines.channels.Channel
class MainActivity : AppCompatActivity() {
lateinit var heavyInitObject: Provider<HeavyInitObject>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
heavyInitObject = initHeavyObject()
}
fun useTheHeavyInitObject() {
// Need it to block here, before work is done.
val hio = heavyInitObject.get()
}
}
fun initHeavyObject(): Provider<HeavyInitObject> {
val cnl = Channel<HeavyInitObject>(Channel.BUFFERED)
//purposely block to ensure to ensure it is initialized
val provider = Provider { runBlocking { cnl.receive().also { cnl.close() }}}
HeavyInitObject.get(object : HeavyInitObject.HeavyInitObjectListener{
override fun onReady(heavyInitObject: HeavyInitObject) = runBlocking {
cnl.send(heavyInitObject)
}
})
return provider
}
// Mocked library I am using (i.e. I don't have control over the implementation)
class HeavyInitObject {
companion object {
fun get(listener: HeavyInitObjectListener) {
val heavyInitObject = HeavyInitObject()
listener.onReady(heavyInitObject)
}
}
interface HeavyInitObjectListener {
fun onReady(heavyInitObject: HeavyInitObject)
}
}
What I don't like
Should be a val
It naturally really be a val, because the value should never change once initialized.
class MainActivity : AppCompatActivity() {
val heavyInitObject: Provider<HeavyInitObject> = initHeavyObject()
// OR...
val heavyInitObject: HeavyInitObject by lazy {
initHeavyObject().get()
}
The first option seems like it could do too much too fast. Depending on how someone would add MainActivity to the object graph it could really affect startup performance.
The second one is too slow. If we haven't requested the heavy object to be created before it is needed, there will be definite jank in the application when the heavy object is queried the first time.
Is there a good way to have the object be a val while requesting the object to be created in onCreate (understanding that I don't have control over implementation of the underlying library)?
Is channel the right data structure here?
Maybe this is bareable, but I wanted to see if there is a better option. A RENDEZVOUS channel makes more sense, but send suspends until receive is called and I don't want to block anything on thread initializing the object (i.e. since i can't convert the implementation to a suspend function). Switching to a bufferend channel won't block since I only send one element through, but that seems like a hack. What is the best data structure for this task?
Edit:
Thanks to some help in the comments I have improved the second condition (eliminate akward use of channel). I have a couple ideas for how to improve the first condition...
Code for getting rid of channel
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
class MainActivity : AppCompatActivity() {
lateinit var heavyInitObject: Provider<HeavyInitObject>
override suspend fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
heavyInitObject = lifecycleScope.async {deferredHeavyObject()}
}
fun useTheHeavyInitObject() {
// Need it to block here, before work is done.
val hio = heavyInitObject.await()
}
}
suspend fun initHeavyObject(): HeavyInitObject = suspendCancellableCoroutine { continuation ->
HeavyInitObject.get(object : HeavyInitObject.HeavyInitObjectListener {
override fun onReady(heavyInitObject: HeavyInitObject) {
continuation.resume(heavyInitObject)
}
})
}
Code to finalize heavyInitObject
class MainActivity : AppCompatActivity() {
val heavyInitObject by lazy { heavyInitObjectBackingField }
private lateinit var heavyInitObjectBackingField: Deferred<HeavyInitObject>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
heavyInitObjectBackingField = lifecycleScope.async { deferredHeavyObject()}
}}
I basically get a lateinit val that way... and can be confident I won't get an error for it not being initialized. Ideally, it makes me realize this is overcomplicated, because I can't get under the hood and easily seperate the object initialization from the call back initialization.. Unless anyone else has a better idea?
Channel is kind of weird for returning a single item. I would load it in a Deferred privately and publicly expose a suspend getter that awaits the Deferred result. Once the object is ready, it won’t have to be waited for. And since it’s a suspend function, you can access it via a coroutine without unlocking your main thread.
object HeavyObjectCreator {
private val heavyObject: Deferred<HeavyObject> = GlobalScope.async {
// Long running actions to generate the object…
HeavyObject(params)
}
suspend fun getInstance(): HeavyObject =
heavyObject.await()
}
In your activity you can use lifecycleScope.launch to start a coroutine when you need to do a task that uses the object and it can call the above function to get it in a suspending way. If you want to preload the heavy object, you can put the statement HeavyObjectCreator in onCreate of your Application class or your Activity so the creator object will be instantiated and start the coroutine to load the heavy object.
This is just one example of a way to do it for an object that you’ll be reusing on multiple screens. If you intend to load a new heavy object only on screens that need it, I’d consider putting the contents of the class above directly in a ViewModel and use viewModelScope instead of GlobalScope.

AutoClearedValue accessed from another thread after View is Destroyed

I am using AutoClearedValue class from this link and when view is destroyed, backing field becomes null and that is good but i have a thread(actually a kotlin coroutine) that after it is done, it accesses the value(which uses autoCleared) but if before it's Job is done i navigate to another fragment(view of this fragment is destroyed), then it tries to access the value, but since it is null i get an exception and therefore a crash.
what can i do about this?
also for which variables this autoCleared needs to be used? i use it for viewBinding and recyclerview adapters.
You have 2 option:
1- Cancelling all the running job(s) that may access to view after its destruction. override onDestroyView() to do it.
Also, you can launch the coroutines viewLifecycleOwner.lifecycleScope to canceling it self when view destroy.
viewLifecycleOwner.lifecycleScope.launch {
// do sth with view
}
2- (Preferred solution) Use Lifecycle aware components (e.g LiveData) between coroutines and view:
coroutines push the state or data in the live-data and you must observe it with viewLifeCycleOwner scope to update the view.
private val stateLiveData = MutableLiveData<String>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
stateLiveData.observe(viewLifecycleOwner) { value ->
binding.textView.text = value
}
}
private fun fetchSomething() {
lifecycleScope.launch {
delay(10_000)
stateLiveData.value = "Hello"
}
}

ViewModel object attribute uninitialized?

I sometimes get an error of UninitializedPropertyAccessException (from analytics), but the application has never crashed during my experience of using it. I think after application is dropped in background and process is killed then this happens - but I've no way of reproducing this error.
In my Activity, I do following:
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.xxx)
... // doing other stuff
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
viewModel.init(
...,
...,
...,))
setUpObservables()
}
and my MyViewModel looks like:
class MyViewModel : ViewModel() {
var lateinit car: String
fun init(
car: String,
...: String?,
...: Boolean
) {
if (isInitialized) {
return
}
this.car = car
...
isInitialized = true
}
Later on I try to access car object an get an error if it's uninitialized. Question is - how can this happen? car object is initialized when MyViewModel is. Any ideas how to reproduce this? Any ideas how to avoid this?
Ok, So I've added your code like this in a ViewModel on one of my working projects.
lateinit var car: String
fun init(car: String) {
this.car = car
}
And called:
viewModel.init("car")
Log.d("CAR_DEBUG",viewModel.car)
And received in my console the expected output: D/CAR_DEBUG: car. So it works but bugs might appear depending on your implementation.
Also, this is not the correct way to do this, I would suggest using the MutableLiveData observer pattern so that you make sure that any unexpected behaviour won't happen due to screen rotation or activity/fragment recreation. With this in mind change your code to this:
var carObservable: MutableLiveData<String> = MutableLiveData()
fun init(car: String) {
carObservable.value = car
}
And in Activity/Fragment:
viewModel.carObservable.observe(this, Observer {
Log.d("CAR_DEBUG",it)
})
// doesnt matter where your viewModel.init this will still work
viewModel.init("car")
This way even if you call the init function after the observe invocation you are sure that you are notified only when the value changes. This is actually the recommended way of using ViewModels (the reactive way through the observer pattern). And doing this you make sure that even if you rotate the screen or recreate the Activity/Fragment your car variable will be available.

How to use android navigation without binding to UI in ViewModel (MVVM)?

I am using android navigation that was presented at Google I/O 2018 and it seems like I can use it by binding to some view or by using NavHost to get it from Fragment. But what I need is to navigate to another specific view from ViewModel from my first fragment depending on several conditions. For ViewModel, I extend AndroidViewModel, but I cannot understand how to do next. I cannot cast getApplication to Fragment/Activity and I can't use NavHostFragment. Also I cannot just bind navigation to onClickListener because the startFragment contains only one ImageView. How can I navigate from ViewModel?
class CaptionViewModel(app: Application) : AndroidViewModel(app) {
private val dealerProfile = DealerProfile(getApplication())
val TAG = "REGDEB"
fun start(){
if(dealerProfile.getOperatorId().isEmpty()){
if(dealerProfile.isFirstTimeLaunch()){
Log.d(TAG, "First Time Launch")
showTour()
}else{
showCodeFragment()
Log.d(TAG, "Show Code Fragment")
}
}
}
private fun showCodeFragment(){
//??
}
private fun showTour(){
//??
}
}
My Fragment
class CaptionFragment : Fragment() {
private lateinit var viewModel: CaptionViewModel
private val navController by lazy { NavHostFragment.findNavController(this) }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
viewModel = ViewModelProviders.of(this).get(CaptionViewModel::class.java)
return inflater.inflate(R.layout.fragment_caption, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.start()
}
}
I want to keep logic of navigation in ViewModel
How can I navigate from ViewModel?
The answer is please don't. ViewModel is designed to store and manage UI-related data.
New Answer
In my previous answers, I said that we shouldn't navigate from ViewModel, and the reason is because to navigate, ViewModel must have references to Activities/Fragments, which I believe (maybe not the best, but still I believe it) is never a good idea.
But, in recommended app architecture from Google, it mentions that we should drive UI from model. And after I think, what do they mean with this?
So I check a sample from "android-architecture", and I found some interesting way how Google did it.
Please check here: todo-mvvm-databinding
As it turns out, they indeed drive UI from model. But how?
They created an interface TasksNavigator that basically just a navigation interface.
Then in the TasksViewModel, they have this reference to TaskNavigator so they can drive UI without having reference to Activities / Fragments directly.
Finally, TasksActivity implemented TasksNavigator to provide detail on each navigation action, and then set navigator to TasksViewModel.
You can use an optional custom enum type and observe changes in your view:
enum class NavigationDestination {
SHOW_TOUR, SHOW_CODE_FRAGMENT
}
class CaptionViewModel(app: Application) : AndroidViewModel(app) {
private val dealerProfile = DealerProfile(getApplication())
val TAG = "REGDEB"
private val _destination = MutableLiveData<NavigationDestination?>(null)
val destination: LiveData<NavigationDestination?> get() = _destination
fun setDestinationToNull() {
_destination.value = null
}
fun start(){
if(dealerProfile.getOperatorId().isEmpty()){
if(dealerProfile.isFirstTimeLaunch()){
Log.d(TAG, "First Time Launch")
_destination.value = NavigationDestination.SHOW_TOUR
}else{
_destination.value = NavigationDestination.SHOW_CODE_FRAGMENT
Log.d(TAG, "Show Code Fragment")
}
}
}
}
And then in your view observe the viewModel destination variable:
viewModel.destination.observe(this, Observer { status ->
if (status != null) {
viewModel.setDestinationToNull()
status?.let {
when (status) {
NavigationDestination.SHOW_TOUR -> {
// Navigate to your fragment
}
NavigationDestination.SHOW_CODE_FRAGMENT -> {
// Navigate to your fragment
}
}
})
}
If you only have one destination you can just use a Boolean rather than the enum.
There are two ways I can recommend doing this.
Use LiveData to communicate and tell the fragment to navigate.
Create a class called Router and this can contain your navigation logic and reference to the fragment or navigation component. ViewModel can communicate with the router class to navigate.

Categories

Resources