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.
Related
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.
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()
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.
I test LiveData like this.
// MainActivity.kt
class MainActivity : AppCompatActivity() {
val testViewModel: TestViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
testViewModel.testLiveData.value = true
testViewModel.testLiveData.observe(this) {
println("Hello")
}
}
}
// TestViewModel.kt
class TestViewModel : ViewModel(){
val testLiveData = MutableLiveData<Boolean>()
}
I think.... (livedata).observe mean start observe about liveData value change.
I don't think the value changed before the observer is set is not observable.
But, it print hello....
Am I misunderstood about live data observers?
When you start observing a LiveData, if the LiveData has a value already, it will replay that value to the observer immediately. This is by design, because LiveData is typically in a ViewModel that outlives the views. For example, when the screen rotates, all the views are recreated and observations begin again. All the views will be updated with the latest values of the LiveData. If it didn't behave this way, then when the screen rotates and Activities/Fragments are recreated, they would just sit there and have nothing to observe, defeating the purpose of using a ViewModel to retain state that outlives views.
I'm trying to write a unit test for a view model using live data.
LoginViewModel.kt
class LoginViewModel #Inject constructor(
val context: Context
): ViewModel() {
val username = MutableLiveData<String>()
val password = MutableLiveData<String>()
val isLoginButtonEnabled = MediatorLiveData<Boolean>().apply {
fun combineLatest(): Boolean {
return !(username.value.isNullOrEmpty() || password.value.isNullOrEmpty())
}
addSource(username) { this.value = combineLatest() }
addSource(password) { this.value = combineLatest() }
}
init {
username.postValue("test")
password.postValue("test")
}
}
LoginViewModelTest.kt
#RunWith(MockitoJUnitRunner::class)
class LoginViewModelTest {
#Rule
#JvmField
val instantTaskExecutorRole = InstantTaskExecutorRule()
private val context = mock(Context::class.java)
private val loginViewModel = LoginViewModel(context)
#Test
fun loginButtonDisabledOnEmptyUsername() {
val observer = mock<Observer<Boolean>>()
loginViewModel.isLoginButtonEnabled.observeForever(observer)
loginViewModel.username.postValue("")
verify(observer).onChanged(false)
}
}
My unit test throws the following exception at the line username.postValue("test"):
java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details.
The InstantTaskExecutorRule should provide an execution context when using live data, however it doesn't work when initializing live data in the init-block. When omitting the init-block it works as desired, but i need the possibility to initialize live data variables.
Is there any way to make the live data initialization work when unit testing view models?
I managed to unit test my ViewModel that was using LiveData using mentioned rula - InstantTaskExecutorRule. But in my case the rule val declaration was a bit different:
#Suppress("unused")
#get:Rule
val instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule()
Edit:
#Before
#Throws(Exception::class)
fun prepare() {
MockitoAnnotations.initMocks(this)
}
Edit2:
For some weird reason I cannot reproduce this :)
Also, I think that the problem could be because of the way you're initializing your ViewModel -
private val loginViewModel = LoginViewModel(context)
I assume that it initializes too early, thus it's init block gets called too early too. Maybe it's reasonable to create it in the #Before method ? Like:
private lateinit var viewModel: LoginViewModel
#Before
#Throws(Exception::class)
fun prepare() {
loginViewModel = LoginViewModel(context)
}
I was seeing a similar issue when setting a LiveData value during the ViewModel's init. Demigod's solution pointed me in the right direction, but I wanted to explain a bit about what was going on and why in the lifecycle of the testing process.
When you have a ViewModel that sets the LiveData during init, it will be run as soon as the view model is initialized. When you initialize the view model in your unit test using val viewModel = MyViewModel(), that view model is instantiated at the same time as the test class is initialized. The problem there is any rules you may have are initialized at the same time, but are not actually run until after the class is completely initialized, so your ViewModel.init() is happening before the rules actually take effect. This means your live data isn't working on an instant executor, any Rx observables aren't being run on replaced schedulers, etc. So ultimately there are two ways of solving for this:
Define the view model as a lateinit var and initialize the view model as a in the #Before method of your test, which runs after rules are applied, or
Define the view model as a val viewModel by lazy { MyViewModel() }, which won't be run until you actually start calling it in your tests.
I prefer option 2 because it also allows me to set up any test-case-specific preconditions before my view model is ever initialized, and I don't have to do repetitive initialization code (which could be quite verbose) inside every test that requires it.
I had a similar issue and the answer provided by Demigod was not solving it. I finally found out where the devil was hiding so I share it here : my init block was set before the liveData initialization, which works fine when running the app, but not when running tests !
class MyViewModel : ViewModel() {
// init { // <-- Do not put the init block before the liveData
// _myLiveData.postValue("First")
// }
private val _myLiveData: MutableLiveData<String> = MutableLiveData()
val myLiveData: LiveData<String>
get() = _myLiveData
init {
_myLiveData.postValue("First")
}
}