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.
Related
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
}
}
I am discovering Kotlin and android app dev. I fail to get data from my room database (because of Cannot access database on the main thread). So I try with lifecyclescope.
The concerned code, in Fragment onViewCreated function, is :
lifecycleScope.launch {
withContext(Dispatchers.Default) {
val accountConfiguration = viewModel.get();
println("{${accountConfiguration}}")
}
}
The called function (in viewModel) is :
fun get() = viewModelScope.launch {
repository.get()
}
There is the "full" code (simplified), Entity & DAO :
#Entity
data class AccountConfiguration(
#PrimaryKey val server_address: String,
#ColumnInfo(name = "user_name") val user_name: String,
// [...]
)
#Dao
interface AccountConfigurationDao {
#Query("SELECT * FROM accountconfiguration LIMIT 1")
fun flow(): Flow<AccountConfiguration?>
#Query("SELECT * FROM accountconfiguration LIMIT 1")
suspend fun get(): AccountConfiguration?
// [...]
}
Repository :
package fr.bux.rollingdashboard
import androidx.annotation.WorkerThread
import kotlinx.coroutines.flow.Flow
class AccountConfigurationRepository(private val accountConfigurationDao: AccountConfigurationDao) {
val accountConfiguration: Flow<AccountConfiguration?> = accountConfigurationDao.flow()
// [...]
#Suppress("RedundantSuspendModifier")
#WorkerThread
suspend fun get() : AccountConfiguration? {
return accountConfigurationDao.get()
}
}
ViewModel & Factory :
class AccountConfigurationViewModel(private val repository: AccountConfigurationRepository) : ViewModel() {
val accountConfiguration: LiveData<AccountConfiguration?> = repository.accountConfiguration.asLiveData()
// [...]
fun get() = viewModelScope.launch {
repository.get()
}
// [...]
}
class AccountConfigurationViewModelFactory(private val repository: AccountConfigurationRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(AccountConfigurationViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return AccountConfigurationViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Fragment :
class AccountConfigurationFragment : Fragment() {
private var _binding: AccountConfigurationFragmentBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
private val viewModel: AccountConfigurationViewModel by activityViewModels {
AccountConfigurationViewModelFactory(
(activity?.application as RollingDashboardApplication).account_configuration_repository
)
}
lateinit var accountConfiguration: AccountConfiguration
// [...]
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonGoBackMain.setOnClickListener {
findNavController().navigate(R.id.action_AccountConfigurationFragment_to_DashboardFragment)
}
lifecycleScope.launch {
withContext(Dispatchers.Default) {
val accountConfiguration = viewModel.get();
println("{${accountConfiguration}}")
}
}
binding.buttonSave.setOnClickListener {
save()
}
}
// [...]
}
In your current code,
lifecycleScope.launch {
withContext(Dispatchers.Default) {
val accountConfiguration = viewModel.get();
println("{${accountConfiguration}}")
}
}
viewModel.get() is not a suspend function, so it returns immediately and proceeds to the next line. It actually returns the Job created by viewModelScope.launch().
If you want your coroutine to wait for the result before continuing you should make the get() function suspend and return the AccountConfiguration?
suspend fun get(): AccountConfiguration? {
return repository.get()
}
You need not change dispatchers to Dispatchers.Default because Room itself will switch to a background thread before executing any database operation.
Right now if there is a configuration change while coroutines inside lifecyclerScope are running, everything will get cancelled and restarted.
A better way would have been to put the suspending calls inside the ViewModel and expose a LiveData/Flow to the UI.
The problem is the viewModel function :
fun get() = viewModelScope.launch {
repository.get()
}
This function must be the coroutine instead launch the coroutine itself. Correct code is :
suspend fun get(): AccountConfiguration? {
return repository.get()
}
I am trying to update my view according to my data in my ViewModel, using MVVM
I need in the method onCacheReception to update my map whenever zones is changing
ViewModel
class MainViewModel constructor(application: Application) : AndroidViewModel(application),
CacheListener {
private val instance = Initializer.getInstance(application.applicationContext)
private val _zones = MutableLiveData<List<Zone>>()
val zones: LiveData<List<Zone>>
get() = _zones
init {
CacheDispatcher.addCacheListener(this)
}
override fun onCacheReception() {
val zonesFromDB: List<Zone>? = instance.fetchZonesInDatabase()
_zones.value = zonesFromDB
}
}
MainActivity
class MainActivity : AppCompatActivity(), EasyPermissions.PermissionCallbacks, OnMapReadyCallback {
private val mainViewModel: MainViewModel = ViewModelProvider(this).get(MainViewModel(application)::class.java)
private lateinit var initializer: Initializer
private lateinit var map: GoogleMap
private val REQUEST_CODE_LOCATIONS: Int = 100
private val permissionLocationsRationale: String = "Permissions for Fine & Coarse Locations"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (checkForLocationsPermission()) {
setUp()
mapSetUp()
}
mainViewModel.zones.observe(this, Observer { zones ->
zones.forEach {
Log.i("YES DATA", "Data has been updated")
val latLng = it.lat?.let { it1 -> it.lng?.let { it2 -> LatLng(it1, it2) } }
val markerOptions = latLng?.let { it1 -> MarkerOptions().position(it1) }
map.addMarker(markerOptions)
}
})
}
My Log is never displaying and it doesn't seem while debugging that mainView.zones.observe { } is called when I receive some new data in my ViewModel
In the onCacheReception(), replace:
_zones.value = zonesFromDB
by:
_zones.postValue(zonesFromDB)
in case your onCacheReception() function is called from a worker thread.
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")
I think somethings wrong about my code in TeamImplsTest, and i need advice :D
This is my code
API interface
interface API {
#GET("lookupteam.php")
fun getTeam(#Query("id") id: String): Call<TeamModel>
}
TeamPresenter
interface MatchPresenter {
fun loadTeamDetail(team_id: String)
}
TeamImpls
class TeamImpls(val teamView: TeamView) : TeamPresenter {
override fun loadTeamDetail(team_id: String) {
val call = RetrofitConfig().getApi().getTeam(team_id)
call.enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
if (response.isSuccessful()) {
val res = response.body()
res?.let { teamView.onSuccess(it) }
}
}
override fun onFailure(call: Call, t: Throwable) {
Log.e("PrevMatchFragment", t.toString())
}
})
}
}
TeamModel
data class TeamModel(
val teams: ArrayList
)
data class TeamModeLResult(
val idTeam: String,
val strTeam: String,
val strAlternate: String,
val strSport: String,
val strStadium: String,
val strTeamBadge: String
)
And
This my TeamImplsTest
class TeamImplsTest {
#Mock
private lateinit var teamView: TeamView
#Mock
private lateinit var teamPresenter: TeamPresenter
#Before
fun setUp() {
MockitoAnnotations.initMocks(this)
teamPresenter = TeamImpls(teamView)
}
#Test
fun loadTeamDetail() {
val teams = TeamModel(arrayListOf())
val teamId = "133613"
teamPresenter.loadTeamDetail(teamId)
Mockito.verify(teamView).onSuccess(teams)
}
}
i got error
Wanted but not invoked:
teamView.onSuccess(TeamModel(teams=[]));
-> at com.fathurradhy.matchschedule.domain.presenter.TeamImplsTest.loadTeamDetail(TeamImplsTest.kt:34)
Actually, there were zero interactions with this mock.
Wanted but not invoked:
teamView.onSuccess(TeamModel(teams=[]));
-> at com.fathurradhy.matchschedule.domain.presenter.TeamImplsTest.loadTeamDetail(TeamImplsTest.kt:34)
Actually, there were zero interactions with this mock.
You're not mocking the API call as loadTeamDetail creates its own API instance.
To enable you to test the API call behaviour you could provide the API instance through your constructor, e.g.
class TeamImpls(private val api: API, private val teamView: TeamView) : TeamPresenter {
override fun loadTeamDetail(team_id: String) {
val call = api.getTeam(team_id)
This would then allow you to mock the api behaviour and verify the presenter calls the correct method when the call fails/succeeds, e.g.
class TeamImplsTest {
#Mock
private lateinit var teamView: TeamView
#Mock
private lateinit var api: API
#Mock
private lateinit var teamPresenter: TeamPresenter
#Before
fun setUp() {
MockitoAnnotations.initMocks(this)
teamPresenter = TeamImpls(api, teamView)
}
#Test
fun loadTeamDetail() {
val teams = TeamModel(arrayListOf())
val teamId = "133613"
// Use retrofit-mock to create your mockResponse.
// See: https://github.com/square/retrofit/tree/master/retrofit-mock
Mockito.`when`(api.getTeam(teamId)).thenReturn(Calls.response(teams)
teamPresenter.loadTeamDetail(teamId)
Mockito.verify(teamView).onSuccess(teams)
}
}