When will the init{...} be launched in a Class of Kotlin? - kotlin

I run Code A, and get the error Result A. It seems that isRecordingState has't been initialized.
So I modify Code A as Code B, and Code B can run correctly.
In my mind, I can place different functions in any order in a class of Kotlin.
I think init{ } of a Class will be launched after the object has been initialized, so I think I can place init{ } in any place of a class.
What's wrong with my ideas?
Code A
#HiltViewModel
class SoundViewModel #Inject constructor(
#ApplicationContext private val appContext: Context,
...
): ViewModel() {
init { beginSoundDensity() }
private val _timeXState = MutableStateFlow(0)
var isRecordingState by mutableStateOf(false)
private set
private var myJob: Job?=null
fun beginSoundDensity() {
if (isRecordingState == false) {
isRecordingState = true
myJob?.cancel()
...
}
}
}
Result A
java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.Object androidx.compose.runtime.State.getValue()' on a null object reference
at info.dodata.soundmeter.presentation.viewmodel.SoundViewModel.isRecordingState(SoundViewModel.kt:245)
at info.dodata.soundmeter.presentation.viewmodel.SoundViewModel.beginSoundDensity(SoundViewModel.kt:81)
at info.dodata.soundmeter.presentation.viewmodel.SoundViewModel.<init>(SoundViewModel.kt:39)
Code B
#HiltViewModel
class SoundViewModel #Inject constructor(
#ApplicationContext private val appContext: Context,
...
): ViewModel() {
private val _timeXState = MutableStateFlow(0)
var isRecordingState by mutableStateOf(false)
private set
private var myJob: Job?=null
init { beginSoundDensity() }
fun beginSoundDensity() {
if (isRecordingState == false) {
isRecordingState = true
myJob?.cancel()
...
}
}
}

The code just runs from top to bottom, so this code for example prints "12345"
fun main() {
A()
}
class A {
init {
print("1")
}
val s2 = printAndReturn("2")
init {
print("3")
}
val s4 = printAndReturn("4")
init {
print("5")
}
private fun printAndReturn(s: String): String {
print(s)
return s
}
}

Related

How to collect StateFlow states from another class?

I have view model like this:
class SimpleViewModel : ViewModel() {
private val _state = MutableStateFlow(false)
val state: StateFlow<Boolean> = _state
}
How can I collect this state's values and call methods from another class like this:
class AnotherClass {
fun doWhenViewModelStateUpdateToTrue()
fun doWhenViewModelStateUpdateToFalse()
}
Your other class needs a reference to the state flow and to a CoroutineScope to run the collection in.
The CoroutineScope should have a lifecycle matching that of this class. So if it's a class you create in an Activity, for example, you would pass lifecycleScope.
class AnotherClass(
private val coroutineScope: CoroutineScope,
private val flowToCollect: Flow<Boolean>
) {
init {
coroutineScope.launch {
flowToCollect.collect {
if (it) doWhenViewModelStateUpdateToTrue()
else doWhenViewModelStateUpdateToFalse()
}
}
}
fun doWhenViewModelStateUpdateToTrue() {
//...
}
fun doWhenViewModelStateUpdateToFalse() {
//...
}
}

Mockk anonymous class

I'm trying to mock anonymous class created in testInstance Sample:
class SomeClass(val someValue:SomeType) :SomeAnotherClass(){
val anonymousClass = object : AnotherClass{
override anotherMethod() { }
}
override fun someMethod(someValue) = anonymousClass.someMethod(someValue)
}
And test class:
class SomeClassTest {
private val someValue: SomeType = mockk()
private val testInstance = spyk(SomeClass(someValue), recordPrivateCalls = true)
#Test
fun `test method`(){
mockkConstructor(CustomTlsSocketFactory::class)
every { anyConstructed<AnotherClass>().someMethod(someValue) } returns mockk()
testInstance.someMethod(someValue)
verify { anyConstructed<AnotherClass>().someMethod(someValue) }
}
}
And for some reason anonymousClass.someMethod(someValue) is trying to call original method not mockk.
Calling testInstance.anonymousClass.isMock is false

Listening to coroutine from view cant be done from the views init

I am trying to listen to my ViewModels MutableStateFlow from my FlutterSceneView. But I get the following error when trying to set the listener from the views init:
Suspend function 'listenToBackgroundColor' should be called only from a coroutine or another suspend function
class FlutterSceneView(context: Context, private val viewModel: FlutterSceneViewModelType): PlatformView {
private val context = context
private val sceneView = SceneView(context)
init {
listenToBackgroundColor() // Error here
}
private suspend fun listenToBackgroundColor() {
viewModel.colorFlow.collect {
val newColor = Color.parseColor(it)
sceneView.setBackgroundColor(newColor)
}
}
}
My ViewModel:
interface FlutterSceneViewModelType {
var colorFlow: MutableStateFlow<String>
}
class FlutterSceneViewModel(private val database: Database): FlutterSceneViewModelType, ViewModel() {
override var colorFlow = MutableStateFlow<String>("#FFFFFF")
init {
listenToBackgroundColorFlow()
}
private fun listenToBackgroundColorFlow() {
database.backgroundColorFlow.watch {
colorFlow.value = it.hex
}
}
}
the .watch call is a helper I have added so that this can be exposed to iOS using Kotlin multi-platform, it looks as follows but I can use collect instead if necessary:
fun <T> Flow<T>.asCommonFlow(): CommonFlow<T> = CommonFlow(this)
class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
fun watch(block: (T) -> Unit): Closeable {
val job = Job()
onEach {
block(it)
}.launchIn(CoroutineScope(Dispatchers.Main + job))
return object : Closeable {
override fun close() {
job.cancel()
}
}
}
}
I resolved this by using viewModel context:
private fun listenToBackgroundColor() {
viewModel.colorFlow.onEach {
val newColor = Color.parseColor(it)
sceneView.setBackgroundColor(newColor)
}.launchIn(viewModel.viewModelScope)
}
I had to import the following into my ViewModel:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
from:
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0")

Test a view model with livedata, coroutines (Kotlin)

I've been trying to test my view model for several days without success.
This is my view model :
class AdvertViewModel : ViewModel() {
private val parentJob = Job()
private val coroutineContext: CoroutineContext
get() = parentJob + Dispatchers.Default
private val scope = CoroutineScope(coroutineContext)
private val repository : AdvertRepository = AdvertRepository(ApiFactory.Apifactory.advertService)
val advertContactLiveData = MutableLiveData<String>()
fun fetchRequestContact(requestContact: RequestContact) {
scope.launch {
val advertContact = repository.requestContact(requestContact)
advertContactLiveData.postValue(advertContact)
}
}
}
This is my repository :
class AdvertRepository (private val api : AdvertService) : BaseRepository() {
suspend fun requestContact(requestContact: RequestContact) : String? {
val advertResponse = safeApiCall(
call = {api.requestContact(requestContact).await()},
errorMessage = "Error Request Contact"
)
return advertResponse
}
}
This is my view model test :
#RunWith(JUnit4::class)
class AdvertViewModelTest {
private val goodContact = RequestContact(...)
private lateinit var advertViewModel: AdvertViewModel
private var observer: Observer<String> = mock()
#get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
#Before
fun setUp() {
advertViewModel = AdvertViewModel()
advertViewModel.advertContactLiveData.observeForever(observer)
}
#Test
fun fetchRequestContact_goodResponse() {
advertViewModel.fetchRequestContact(goodContact)
val captor = ArgumentCaptor.forClass(String::class.java)
captor.run {
verify(observer, times(1)).onChanged(capture())
assertEquals("someValue", value)
}
}
}
The method mock() :
inline fun <reified T> mock(): T = Mockito.mock(T::class.java)
I got this error :
Wanted but not invoked: observer.onChanged();
-> at com.vizzit.AdvertViewModelTest.fetchRequestContact_goodResponse(AdvertViewModelTest.kt:52)
Actually, there were zero interactions with this mock.
I don't understand how to retrieve the result of my query.
You would need to write a OneTimeObserver to observe livedata from the ViewModel
class OneTimeObserver<T>(private val handler: (T) -> Unit) : Observer<T>, LifecycleOwner {
private val lifecycle = LifecycleRegistry(this)
init {
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}
override fun getLifecycle(): Lifecycle = lifecycle
override fun onChanged(t: T) {
handler(t)
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
}
After that you can write an extension function:
fun <T> LiveData<T>.observeOnce(onChangeHandler: (T) -> Unit) {
val observer = OneTimeObserver(handler = onChangeHandler)
observe(observer, observer)
}
Than you can check this ViewModel class class that I have from a project to check what's going on with your LiveData after you act (when) with invoking a method.
As for your error, it just says that the onChanged() method is not being called ever.

MutableLiveData for collections

I request data from server by bunches and store it in the array.To track fetching of the next bunch of the data I have this class.In the addItems method I notify diffObservers and pass list of new items:
class PackItems:MutableLiveData<ArrayList<GetPacksResponse.PackData>>() {
private var diffObservers=ArrayList<Observer<List<GetPacksResponse.PackData>>>()
private var active=false
fun observeItems(owner: LifecycleOwner, valueObserver:Observer<List<GetPacksResponse.PackData>>,diffObserver:Observer<List<GetPacksResponse.PackData>>) {
super.observe(owner,valueObserver)
diffObservers.add(diffObserver)
}
override fun removeObservers(owner: LifecycleOwner) {
super.removeObservers(owner)
diffObservers= ArrayList()
}
fun addItems(toAdd:List<GetPacksResponse.PackData>) {
value?.addAll(toAdd)
if (active)
for (observer in diffObservers)
observer.onChanged(toAdd)
}
override fun onActive() {
super.onActive()
active=true
}
override fun onInactive() {
super.onInactive()
active=false
}
}
The problem is PackItems is MutableLiveData and it's not good practice to expose it.Is there way to cast it to LiveData?Like usually we do:
private val _items = MutableLiveData<List<Int>>()
val items: LiveData<List<Int>> = _items
UPD:Ideally would be if I could expose completely immutable LiveData.But I can't just write
private val _packs:PackItems=PackItems()
val packs:LiveData<ArrayList<GetPacksResponse.PackData>>
get()=_packs
Because in this case packs won't contain observeItems method.Therefore there must be custom class derived from LiveData like:
open class PackItems: LiveData<ArrayList<GetPacksResponse.PackData>>() {
protected var active=false
protected var diffObservers = ArrayList<Observer<List<GetPacksResponse.PackData>>>()
fun observeItems(owner: LifecycleOwner, valueObserver: Observer<List<GetPacksResponse.PackData>>, diffObserver: Observer<List<GetPacksResponse.PackData>>) {
super.observe(owner,valueObserver)
diffObservers.add(diffObserver)
}
//...
}
class MutablePackItems: PackItems() {
fun addItems(toAdd:List<GetPacksResponse.PackData>) {
value?.addAll(toAdd)
if (active)
for (observer in diffObservers)
observer.onChanged(toAdd)
}
}
But in this case I won't be able to set data because now MutablePackItems is LiveData(immutable) :)
I'd consider using composition instead of inheritance:
class PackItems() {
private val mutableData = MutableLiveData<ArrayList<GetPacksResponse.PackData>>()
val asLiveData: LiveData<ArrayList<GetPacksResponse.PackData>> get() = mutableData
...
fun observeItems(owner: LifecycleOwner, valueObserver:Observer<List<GetPacksResponse.PackData>>,diffObserver:Observer<List<GetPacksResponse.PackData>>) {
mutableData.observe(owner,valueObserver)
diffObservers.add(diffObserver)
}
fun removeObservers(owner: LifecycleOwner) {
mutableData.removeObservers(owner)
diffObservers = ArrayList()
}
// etc
}
EDIT: to set active as in your original code, may be a bit nastier:
private val mutableData = object : MutableLiveData<ArrayList<GetPacksResponse.PackData>>() {
override fun onActive() {
super.onActive()
active = true
}
override fun onInactive() {
super.onInactive()
active = false
}
}
EDIT 2:
but the main problem is I need to return custom LiveData class with custom observeItems method
The point is that you don't necessarily. Whenever you'd call LiveData's method (e.g. observe), just call items.asLiveData.observe(...) instead. If you want to pass it to another method foo accepting LiveData, call foo(items.asLiveData).
In principle, you could modify this approach by extending LiveData and delegating all calls to mutableData:
class PackItems(): LiveData<ArrayList<GetPacksResponse.PackData>>() {
private val mutableData = MutableLiveData<ArrayList<GetPacksResponse.PackData>>()
...
fun observeItems(owner: LifecycleOwner, valueObserver:Observer<List<GetPacksResponse.PackData>>,diffObserver:Observer<List<GetPacksResponse.PackData>>) {
mutableData.observe(owner,valueObserver)
diffObservers.add(diffObserver)
}
override fun observe(owner: LifecycleOwner, observer: ArrayList<GetPacksResponse.PackData>) {
mutableData.observe(owner, observer)
}
override fun removeObservers(owner: LifecycleOwner) {
mutableData.removeObservers(owner) // not super!
diffObservers = ArrayList()
}
// etc
}
but I don't think it's a good idea.