I passed a list to the adapter from an activity and on calling notifydatasetchanged, recycler view is still empty, It is not updating the view.
var list : ArrayList<StationBean> = ArrayList()
override fun getItemCount(): Int {
return list.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(context).inflate(R.layout.stations_adapter, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.stationsName.text = list.get(position).stationName
holder.cityName.text = list.get(position).latitude.toString()
}
fun add_data(data : StationsListDataClass)
{
list = data.stationBeanList
notifyDataSetChanged()
Log.d("List", list.toString())
}
}
////// . Setting the adapter .
private var adapter: StationsListAdapter = StationsListAdapter(this)
recycler_view_stationsList.apply {
layoutManager = LinearLayoutManager(this.context)
this.adapter = adapter
}
///// Layout File
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recycler_view_stationsList"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible"
/>
// . Main Activity Full Code , I used inject for calling presenter,The main problem is in the setRecycler function
#Inject
lateinit var stationsListPresenter: StationsListPresenter
private lateinit var station_adapter: StationsListAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_stations_list)
setupRecycler()
stationsListPresenter.downLoadStationsListUsingRetrofit()
}
private fun setupRecycler()
{
val recyclerView : RecyclerView = findViewById(R.id.recycler_view_stations_list)
recyclerView.apply {
layoutManager = LinearLayoutManager(this#StationsListActivity) // replace MainActivity with your activity name
station_adapter = StationsListAdapter(this#StationsListActivity)
}
recyclerView.adapter = station_adapter
}
override fun addNewsToRecyclerView(stationsList: StationsListDataClass)
{
station_adapter.add_data(stationsList)
}
override fun makeInjection(activityComponent: ActivityComponent) {
activityComponent.inject(this)
}
The View is still empty after calling notifydatasetchanged()
and I am not getting any error.
The List have around 800 objects.
Recycler View is showing null pointer exception, even after binding view using findviewbyID
Please post your xml layout and/or setting up recycler view code. Maybe there is no LayoutManager set. You may set it in xml or in the activity/fragment:
recyclerView.setLayoutManager(new LinearLayoutManager(this));
also try clearing your list in add_data()
list.clear()
list.addAll(..)
And modify your add_data function like this
fun add_data(data : StationsListDataClass){
list.clear()
list.addAll(data.stationBeanList)
notifyDataSetChanged()
Log.d("List", list.toString())
}
Update
Modify your setupRecycler like below
private fun setupRecycler(){
var recyclerView = findViewById(R.id.recycler_view_stations_list)
station_adapter = StationsListAdapter(this#StationsListActivity)
val layoutManager = LinearLayoutManager(this#StationsListActivity)
recyclerView?.layoutManager = layoutManager
recyclerView?.adapter = station_adapter
}
Related
I want to update list from paging with search query result,basically I had success to display item paging to lazyverticalgrid without searchQuery variable. The problem is when I click search button from keyboard virtual, the items not update in compose
HomeViewModel.kt
#HiltViewModel
class HomeViewModel #Inject constructor(
savedStateHandle: SavedStateHandle,
private val productRepository: ProductRepository,
private val dataUserDao: DataUserDao
) : ViewModel() {
private val _searchLiveData = savedStateHandle.getLiveData("search", "")
val searchLiveData: LiveData<String> get() = _searchLiveData
private val _productList = MutableStateFlow<PagingData<Product>>(PagingData.empty())
val productList = _productList.asStateFlow()
init {
getDashboard()
getBanner()
searchProduct("") // this work!
}
fun searchProduct(s: String) { // when this execute again with value the product list not recomposed
viewModelScope.launch {
productRepository.getProducts(0, s, "", "").collect { data ->
_productList.value = data
}
}
}
The collectAsLazyPagingItems function for product list is work when search query is empty, but when search query is not empty collectAsLazyPagingItems function is not triggered
ShopScreen.kt
#HomeNavGraph
#Destination
#Composable
fun ShopScreen(
navigator: DestinationsNavigator,
viewModel: HomeViewModel = hiltViewModel()
) {
.....
val lazyGridState = rememberLazyGridState(
val productListItems: LazyPagingItems<Product> =
viewModel.productList.collectAsLazyPagingItems()
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
...
LazyVerticalGrid(
state = lazyGridState,
columns = GridCells.Adaptive(screenWidth),
verticalArrangement = Arrangement.spacedBy(10.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(vertical = 16.dp)
) {
Log.e("TAG", "ShopScreen: ${state.productList.size}")
items(productListItems.itemCount) { index ->
CardProductShopGrid(product = productListItems[index]!!, onClick = {})
}
}
}
}
}
would someone be able to show me how to make the getMovies function in this viewModel testable? I can't get the unit tests to await the coroutines properly..
(1) I'm pretty sure I have to create a test-CoroutineScope and a normal lifeCycle-CoroutineScope, as seen in this Medium Article.
(2) Once the scope definitions are made, I'm also unsure how to tell getMovies() which scope it should be using given a normal app context or a test context.
enum class MovieApiStatus { LOADING, ERROR, DONE }
class MovieListViewModel : ViewModel() {
var pageCount = 1
private val _status = MutableLiveData<MovieApiStatus>()
val status: LiveData<MovieApiStatus>
get() = _status
private val _movieList = MutableLiveData<List<Movie>>()
val movieList: LiveData<List<Movie>>
get() = _movieList
// allows easy update of the value of the MutableLiveData
private var viewModelJob = Job()
// the Coroutine runs using the Main (UI) dispatcher
private val coroutineScope = CoroutineScope(
viewModelJob + Dispatchers.Main
)
init {
Log.d("list", "in init")
getMovies(pageCount)
}
fun getMovies(pageNumber: Int) {
coroutineScope.launch {
val getMoviesDeferred =
MovieApi.retrofitService.getMoviesAsync(page = pageNumber)
try {
_status.value = MovieApiStatus.LOADING
val responseObject = getMoviesDeferred.await()
_status.value = MovieApiStatus.DONE
............
} catch (e: Exception) {
_status.value = MovieApiStatus.ERROR
................
}
}
pageCount = pageNumber.inc()
}
...
}
it uses this API service...
package com.example.themovieapp.network
import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import kotlinx.coroutines.Deferred
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
private const val BASE_URL = "https://api.themoviedb.org/3/"
private const val API_key = ""
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.baseUrl(BASE_URL)
.build()
interface MovieApiService{
//https://developers.themoviedb.org/3/movies/get-top-rated-movies
//https://square.github.io/retrofit/2.x/retrofit/index.html?retrofit2/http/Query.html
#GET("movie/top_rated")
fun getMoviesAsync(
#Query("api_key") apiKey: String = API_key,
#Query("language") language: String = "en-US",
#Query("page") page: Int
): Deferred<ResponseObject>
}
/*
Because this call is expensive, and the app only needs
one Retrofit service instance, you expose the service to the rest of the app using
a public object called MovieApi, and lazily initialize the Retrofit service there
*/
object MovieApi {
val retrofitService: MovieApiService by lazy {
retrofit.create(MovieApiService::class.java)
}
}
I'm simply trying to create a test which asserts the liveData 'status' is DONE after the function.
Here is the Project Repository
First you need to make your coroutine scope injectable somehow, either by creating a provider for it manually, or using an injection framework like dagger. That way, when you test your ViewModel, you can override the coroutine scope with a test version.
There are a few choices to do this, you can simply make the ViewModel itself injectable (article on that here: https://medium.com/chili-labs/android-viewmodel-injection-with-dagger-f0061d3402ff)
Or you can manually create a ViewModel provider and use that where ever it's created. No matter what, I would strongly advise some form of dependency injection in order to achieve real testability.
Regardless, your ViewModel needs to have its CoroutineScope provided, not instantiate the coroutine scope itself.
In other words you might want
class MovieListViewModel(val couroutineScope: YourCoroutineScope) : ViewModel() {}
or maybe
class MovieListViewModel #Inject constructor(val coroutineScope: YourCoroutineScope) : ViewModel() {}
No matter what you do for injection, the next step is to create your own CoroutineScope interface that you can override in the test context. For example:
interface YourCoroutineScope : CoroutineScope {
fun launch(block: suspend CoroutineScope.() -> Unit): Job
}
That way when you use the scope for your app, you can use one scope, say, lifecycle coroutine scope:
class LifecycleManagedCoroutineScope(
private val lifecycleCoroutineScope: LifecycleCoroutineScope,
override val coroutineContext: CoroutineContext = lifecycleCoroutineScope.coroutineContext) : YourCoroutineScope {
override fun launch(block: suspend CoroutineScope.() -> Unit): Job = lifecycleCoroutineScope.launchWhenStarted(block)
}
And for your test, you can use a test scope:
class TestScope(override val coroutineContext: CoroutineContext) : YourCoroutineScope {
val scope = TestCoroutineScope(coroutineContext)
override fun launch(block: suspend CoroutineScope.() -> Unit): Job {
return scope.launch {
block.invoke(this)
}
}
}
Now, since your ViewModel is using a scope of type YourCoroutineScope, and since, in the examples above, both the lifecycle and test version implement the YourCoroutineScope interface, you can use different versions of the scope in different situations, i.e. app vs test.
Ok, thanks to Dapp's answer, I was able to write some tests which seem to be awaiting the function Properly.
Here is a copy of what I did :)
enum class MovieApiStatus { LOADING, ERROR, DONE }
class MovieListViewModel(val coroutineScope: ManagedCoroutineScope) : ViewModel() {
//....creating vars, livedata etc.
init {
getMovies(pageCount)
}
fun getMovies(pageNumber: Int) =
coroutineScope.launch{
val getMoviesDeferred =
MovieApi.retrofitService.getMoviesAsync(page = pageNumber)
try {
_status.value = MovieApiStatus.LOADING
val responseObject = getMoviesDeferred.await()
_status.value = MovieApiStatus.DONE
if (_movieList.value == null) {
_movieList.value = ArrayList()
}
pageCount = pageNumber.inc()
_movieList.value = movieList.value!!.toList().plus(responseObject.results)
.sortedByDescending { it.vote_average }
} catch (e: Exception) {
_status.value = MovieApiStatus.ERROR
_movieList.value = ArrayList()
}
}
fun onLoadMoreMoviesClicked() =
getMovies(pageCount)
//...nav functions, clearing functions etc.
}
and here are the test cases
#ExperimentalCoroutinesApi
#RunWith(MockitoJUnitRunner::class)
class MovieListViewModelTest {
#get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
private val testDispatcher = TestCoroutineDispatcher()
private val managedCoroutineScope: ManagedCoroutineScope = TestScope(testDispatcher)
lateinit var viewModel: MovieListViewModel
#Before
fun setup() {
//resProvider.mockColors()
Dispatchers.setMain(testDispatcher)
viewModel = MovieListViewModel(managedCoroutineScope)
}
#After
fun tearDown() {
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
#ExperimentalCoroutinesApi
#Test
fun getMoviesTest() {
managedCoroutineScope.launch {
assertTrue(
"initial List, API status: ${viewModel.status.getOrAwaitValue()}",
viewModel.status.getOrAwaitValue() == MovieApiStatus.DONE
)
assertTrue(
"movieList has ${viewModel.movieList.value?.size}, != 20",
viewModel.movieList.value?.size == 20
)
assertTrue(
"pageCount = ${viewModel.pageCount}, != 2",
viewModel.pageCount == 2
)
viewModel.onLoadMoreMoviesClicked()
assertTrue(
"added to list, API status: ${viewModel.status.getOrAwaitValue()}",
viewModel.status.getOrAwaitValue() == MovieApiStatus.DONE
)
assertTrue(
"movieList has ${viewModel.movieList.value?.size}, != 40",
viewModel.movieList.value?.size == 40
)
}
}
}
It took some trial and error playing around with the Scopes.. runBlockingTest{} was causing an issue 'Exception: job() not completed'..
I also had to create a viewModel factory in order for the fragment to create the viewModel for when the app is running normally..
Project Repo
I would like to test case when server does not return response, and we trigger the next network call ( like for example search query).
So we basically have a method inside ViewModel and Retrofit method
interface RetrofitApi {
#GET("Some Url")
suspend fun getVeryImportantStuff(): String
}
class TestViewModel(private val api: RetrofitApi) : ViewModel() {
private var askJob: Job? = null
fun load(query: String) {
askJob?.cancel()
askJob = viewModelScope.launch {
val response = api.getVeryImportantStuff()
//DO SOMETHING WITH RESPONSE
}
}
}
And I would like to test case when new query is asked, and the old one didn't returns.
for case when response returns test is easy
#Test
fun testReturnResponse() {
runBlockingTest {
//given
val mockApi:RetrofitApi = mock()
val viewModel = TestViewModel(mockApi)
val response = "response from api"
val query = "fancy query"
whenever(mockApi.getVeryImportantStuff()).thenReturn(response)
//when
viewModel.load(query)
//then
//verify what happens
}
}
But I don't know how to mock suspend function that did't come back, and test case when new request is triggered like this
#Test
fun test2Loads() {
runBlockingTest {
//given
val mockApi:RetrofitApi = mock()
val viewModel = TestViewModel(mockApi)
val response = "response from api"
val secondResponse = "response from api2"
val query = "fancy query"
whenever(mockApi.getVeryImportantStuff())
.thenReturn(/* Here return some fancy stuff that is suspend* or something like onBlocking{} stub but not blocking but dalayed forever/)
.thenReturn(secondResponse)
//when
viewModel.load(query)
viewModel.load(query)
//then
//verify that first response did not happens , and only second one triggered all the stuff
}
}
Any ideas ?
EDIT: I'm not really attached to mockito, any mock library will be good :)
regards
Wojtek
I came up with kind of solution to the problem, but slightly different than I was thinking at the beginning
interface CoroutineUtils {
val io: CoroutineContext
}
interface RetrofitApi {
#GET("Some Url")
suspend fun getVeryImportantStuff(query: String): String
}
class TestViewModel(private val api: RetrofitApi,
private val utils: CoroutineUtils) : ViewModel() {
private val text = MutableLiveData<String>()
val testStream: LiveData<String> = text
private var askJob: Job? = null
fun load(query: String) {
askJob?.cancel()
askJob = viewModelScope.launch {
val response = withContext(utils.io) { api.getVeryImportantStuff(query) }
text.postValue(response)
}
}
}
And the test scenario would look like this
class TestViewModelTest {
#get:Rule
val coroutineScope = MainCoroutineScopeRule()
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var retrofit: RetrofitApi
lateinit var utils: CoroutineUtils
val tottalyDifferentDispatcher = TestCoroutineDispatcher()
lateinit var viewModel: TestViewModel
#Before
fun setup() {
retrofit = mock()
utils = mock()
viewModel = TestViewModel(retrofit, utils)
}
#UseExperimental(ExperimentalCoroutinesApi::class)
#Test
fun test2Loads() {
runBlockingTest {
//given
val response = "response from api"
val response2 = "response from api2"
val query = "fancy query"
val query2 = "fancy query2"
whenever(utils.io)
.thenReturn(tottalyDifferentDispatcher)
val mutableListOfStrings = mutableListOf<String>()
whenever(retrofit.getVeryImportantStuff(query)).thenReturn(response)
whenever(retrofit.getVeryImportantStuff(query2)).thenReturn(response2)
//when
viewModel.testStream.observeForever {
mutableListOfStrings.add(it)
}
tottalyDifferentDispatcher.pauseDispatcher()
viewModel.load(query)
viewModel.load(query2)
tottalyDifferentDispatcher.resumeDispatcher()
//then
mutableListOfStrings shouldHaveSize 1
mutableListOfStrings[0] shouldBe response2
verify(retrofit, times(1)).getVeryImportantStuff(query2)
}
}
}
It is not exactly what I wanted, because retrofit call is not triggered when load method is called for the first time, but it is the closest solution.
What would be a perfect test for me will be assertion that retrofit was called twice , but only the second one returned to ViewModel. Solution for that will be to wrap Retrofit around method that returns suspend function like this
interface RetrofitWrapper {
suspend fun getVeryImportantStuff(): suspend (String)->String
}
class TestViewModel(private val api: RetrofitWrapper,
private val utils: CoroutineUtils) : ViewModel() {
private val text = MutableLiveData<String>()
val testStream: LiveData<String> = text
private var askJob: Job? = null
fun load(query: String) {
askJob?.cancel()
askJob = viewModelScope.launch {
val veryImportantStuff = api.getVeryImportantStuff()
val response = withContext(utils.io) {
veryImportantStuff(query)
}
text.postValue(response)
}
}
}
and test for it
#Test
fun test2Loads() {
runBlockingTest {
//given
val response = "response from api"
val response2 = "response from api2"
val query = "fancy query"
val query2 = "fancy query2"
whenever(utils.io)
.thenReturn(tottalyDifferentDispatcher)
val mutableListOfStrings = mutableListOf<String>()
whenever(retrofit.getVeryImportantStuff())
.thenReturn(suspendCoroutine {
it.resume { response }
})
whenever(retrofit.getVeryImportantStuff()).thenReturn(suspendCoroutine {
it.resume { response2 }
})
//when
viewModel.testStream.observeForever {
mutableListOfStrings.add(it)
}
tottalyDifferentDispatcher.pauseDispatcher()
viewModel.load(query)
viewModel.load(query2)
tottalyDifferentDispatcher.resumeDispatcher()
//then
mutableListOfStrings shouldHaveSize 1
mutableListOfStrings[0] shouldBe response2
verify(retrofit, times(2)).getVeryImportantStuff()
}
}
But in my opinion it is a little bit too much in interference in code only to be testable. But maybe I'm wrong :P
Looks like you want to test scenario when you have unreachable server, timeout or something similar.
In this case while doing your mock you can say that on first try it returns object and then on second executions throws exception like java.net.ConnectException: Connection timed out.
whenever(mockApi.getVeryImportantStuff())
.thenReturn(someObjet)
.thenThrow(ConnectException("timed out"))
And this this should work but you will have to do try/catch block in ViewModel witch is not ideal. I would suggest you to add additional abstraction.
You could you Repository or UseCase or whatever pattern/name you like to move the network call there. Then introduce sealed class Result to encapsulate behaviour and make your ViewModel more readable.
class TestViewModel(val repo: Repo): ViewModel() {
private var askJob: Job? = null
fun load(query: String) {
askJob?.cancel()
askJob = viewModelScope.launch {
when (repo.getStuff()) {
is Result.Success -> TODO()
is Result.Failure -> TODO()
}
}
}
}
class Repo(private val api: Api) {
suspend fun getStuff() : Result {
return try {
Result.Success(api.getVeryImportantStuff())
} catch (e: java.lang.Exception) {
Result.Failure(e)
}
}
}
sealed class Result {
data class Success<out T: Any>(val data: T) : Result()
data class Failure(val error: Throwable) : Result()
}
interface Api {
suspend fun getVeryImportantStuff() : String
}
With that level of abstraction your ViewModelTest only checks what happens in two cases.
Hope that's helpful!
Is it possible to pass a specific value into an extension function with receiver, in a unit test?
I'm trying to test the folowing subscribe method:
...
private lateinit var subscription: SubscriptionReceiveChannel<UiStateModel>
suspend fun subscribe(model: MainViewModel) {
subscription = model.connect()
subscription.consumeEach { value -> loadView(value) /** or loadView(it) */ }
}
private fun loadView(uiState: UiStateModel) {
when(uiState) {
is Loading -> view.isLoading()
is Error -> view.isError(uiState.exception.localizedMessage)
is Success -> when {
uiState.result != null -> view.isSuccess(uiState.result)
else -> view.isEmpty()
}
}
}
I want to be able to apply a specific value to the consumeEach function, but how can this be done?
Here's my unit test:
...
private val view = mock<MainView>()
private val model = mock<MainViewModel>()
private val subscription = mock<SubscriptionReceiveChannel<UiStateModel>>()
private val presenter = MainPresenter(view)
#Test
fun `When uistate is loading, view should show loading message`() = runBlocking {
// Given
val state = UiStateModel.Loading()
whenever(model.connect()).thenReturn(subscription)
whenever(subscription.consumeEach { any() }).thenAnswer({ state })
// When
presenter.subscribe(model)
// Then
verify(model).connect()
verify(view).isLoading()
verify(view, never()).isSuccess(anyString())
verify(view, never()).isEmpty()
verify(view, never()).isError(anyString())
}
...
I am trying to write proper test for my reactive repository, which just listen for location changes. I don't want to actually listen for location changes and just 'invoke' new location change multiple times with just created custom Location.
Here is the repository and its function for gathering locations:
interface RxLocationRepository {
#SuppressLint("MissingPermission")
fun onLocationUpdate(): Observable<Location>
fun stopLocationUpdates()
}
Here is the implementation of it, which actually DON'T MATTER because i don't want to listen for real location updates, but i want to show you that it is just reactive implementation for listen to LocationManager updates:
class LocationNativeRepository(
val locationManager: LocationManager,
val geoEventsDistanceMeters: Int,
val geoEventsIntervalSeconds: Int) : RxLocationRepository{
var locationToPopulate: Location = Location(LocationManager.GPS_PROVIDER)
lateinit var mLocationCallbackNativeApi: LocationListener
private val subject: BehaviorSubject<Location> = BehaviorSubject.createDefault(locationToPopulate)
var locationEmitter: Observable<Location> = subject.hide()
init {
configureNativeLocationEmitter()
}
override fun onLocationUpdate(): Observable<Location> {
return locationEmitter
}
#SuppressLint("MissingPermission")
override fun stopLocationUpdates() {
locationManager.removeUpdates(mLocationCallbackNativeApi)
}
#SuppressLint("MissingPermission")
private fun configureNativeLocationEmitter() {
mLocationCallbackNativeApi = object : LocationListener {
override fun onLocationChanged(location: Location) {
subject.onNext(location)
}
override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
}
try {
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
(geoEventsIntervalSeconds * 1000).toLong(),
geoEventsDistanceMeters.toFloat(),
mLocationCallbackNativeApi,
Looper.getMainLooper())
} catch (ignored: IllegalArgumentException) {
ignored.printStackTrace()
}
try {
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
(geoEventsIntervalSeconds * 1000).toLong(),
geoEventsDistanceMeters.toFloat(),
mLocationCallbackNativeApi,
Looper.getMainLooper())
} catch (ignored: IllegalArgumentException) {
ignored.printStackTrace()
}
}
}
So how i can invoke this repository in my test to actually trigger the onLocationUpdate() method? So for example i will make it emit 3 times location like this:
val location = Location("test").apply {
latitude = 1.234
longitude = 5.678
accuracy = 20f
time = Date().time
}
Create a PublishSubject<Location> variable. Create an anonymous sub-class of RxLocationRepository, where the onLocationUpdate() method returns the variable. Then emit values into the variable to provide new locations.