Migrate to ObserveAsState in Jetpack Compose - null - state

I am a beginner trying to learn Kotlin by changing an old tutorial to Compose.
I have a ViewModel with
private val _registerStatus = MutableLiveData<Resource<String>>()
val registerStatus: LiveData<Resource<String>> = _registerStatus
fun register(email: String, password: String, repeatedPassword: String) {
_registerStatus.postValue(Resource.loading(null))
if(email.isEmpty() || password.isEmpty() || repeatedPassword.isEmpty()) {
_registerStatus.postValue(Resource.error("Please fill out all the fields", null))
return
}
if(password != repeatedPassword) {
_registerStatus.postValue(Resource.error("The passwords do not match", null))
return
}
viewModelScope.launch {
val result = repository.register(email, password)
_registerStatus.postValue(result)
}
}
and Fragment with:
viewModel.registerStatus.observe(viewLifecycleOwner, Observer { result ->
result?.let {
when(result.status) {
Status.SUCCESS -> {
registerProgressBar.visibility = View.GONE
showSnackbar(result.data ?: "Successfully registered an account")
}
Status.ERROR -> {
registerProgressBar.visibility = View.GONE
showSnackbar(result.message ?: "An unknown error occurred")
}
Status.LOADING -> {
registerProgressBar.visibility = View.VISIBLE
}
}
}
})
How could I adapt this code to use Jetpack Compose?
I understand I need to use "ObserveAsState" in a Composable :
registerViewModel.registerStatus.observeAsState()
Truth is, I think I don't really understand the nullable issue, or what val result is doing, or what result -> result?.let is doing, except being some way to pass a non-null value in? If removed, I must make registerStatus.status non-null or safe. So I can do the below:
#Composable
fun subscribeToObservers() {
val registerStatus by registerViewModel.registerStatus.observeAsState()
when(registerStatus!!.status){
Status.SUCCESS -> {
}
Status.ERROR -> {
}
Status.LOADING -> {
}
}
}
, or do I need to get the "result" value over?
Anything to help me toward understanding the issues better would be really appreciated.

You should avoid using !! as that means you know that that variable is never null. Your application is going to crash if the variable is ever null when going through this piece of code.
I want also to note that logically, you might not be able to find a scenario in which your variable is going to be null so you might get tempted to use !! but it is better to avoid it just in case.
Using nullableVariable?.let { -> nonNullVariable } is much safer as it only runs if the variable is not null. Unlike !!, it won't cause a crash if the variable is null.
I would write the code like this:
#Composable
fun subscribeToObservers() {
val registerStatus by registerViewModel.registerStatus.observeAsState()
registerStatus?.let { nonNullRegisterStatus ->
when(nonNullRegisterStatus) {
Status.SUCCESS -> {}
Status.ERROR -> {}
Status.LOADING -> {}
}
}
}

There are two ways you can approach this:
Compose style
ViewModel
private val _registerStatus : MutableState<Resource<String>> = mutableStateOf(Resource.init(null))
val registerStatus: State<Resource<String>> = _registerStatus
fun register(email: String, password: String, repeatedPassword: String) {
_registerStatus.value = Resource.loading(null)
if(email.isEmpty() || password.isEmpty() || repeatedPassword.isEmpty()) {
_registerStatus.value = Resource.error("Please fill out all the fields", null)
return
}
if(password != repeatedPassword) {
_registerStatus.value = Resource.error("The passwords do not match", null)
return
}
viewModelScope.launch {
val result = repository.register(email, password)
_registerStatus.value = result
}
}
Composable
#Composable
fun subscribeToObservers() {
val registerStatus by viewModel.registerStatus
when(registerStatus.status){
Status.INIT -> {}
Status.SUCCESS -> {}
Status.ERROR -> {}
Status.LOADING -> {}
}
}
You can make _registerStatus not null in this way or if you initialize it as private val _registerStatus : MutableState<Resource<String>?> = mutableStateOf(null) handle null like:
#Composable
fun subscribeToObservers() {
val registerStatus by viewModel.registerStatus
when(registerStatus.status){
Status.INIT -> {}
Status.SUCCESS -> {}
Status.ERROR -> {}
Status.LOADING -> {}
null -> {}
}
}
Kotlin style
ViewModel
private val _registerStatus : MutableStateFlow<Resource<String>> = mutableStateOf(Resource.init(null))
val registerStatus: StateFlow<Resource<String>> = _registerStatus
fun register(email: String, password: String, repeatedPassword: String) {
_registerStatus.tryEmit(Resource.loading(null))
if(email.isEmpty() || password.isEmpty() || repeatedPassword.isEmpty()) {
_registerStatus.tryEmit(Resource.error("Please fill out all the fields", null))
return
}
if(password != repeatedPassword) {
_registerStatus.tryEmit(Resource.error("The passwords do not match", null))
return
}
viewModelScope.launch {
val result = repository.register(email, password)
_registerStatus.tryEmit(result)
}
}
Composable
#Composable
fun subscribeToObservers() {
val registerStatus by viewModel.registerStatus.collectAsState()
when(registerStatus.status){
Status.INIT -> {}
Status.SUCCESS -> {}
Status.ERROR -> {}
Status.LOADING -> {}
null -> {}
}
}
MutableStateFlow are thread safe and kotlin's solution to xJava This method is thread-safe and can be safely invoked from concurrent coroutines without external synchronization.
Kotlin provides one more type of flow called SharedFlow and it's upon your use case to use SharedFlow or StateFlow.
https://developer.android.com/kotlin/flow/stateflow-and-sharedflow
The main difference between SharedFlow and StateFlow

Related

return a new list from function in a for loop to pass in the updated list

I have a method that take a list as a parameter that performs some operation on it and returns the new list. However, in my for..loop I would to keep passing in the updated list until the for..loop has completed.
Is there a way to do this?
fun main(args: Array<String>) {
val listOfSeatRows = (1..127).toList()
// Just loop until all the listOfPass has completed.
listOfPass.forEach { seatPass ->
val seat = Seat.valueOf(seatPass.toString())
// I want to pass in the new updated list not the same list
getListOfSeatRows(listOfSeatRows, seat)
}
}
This method takes the list and return a updated list. However, in the for..loop above I would like to pass in the list that is returned from this method
private fun getListOfSeatRows(listOfSeat: List<Int>, seatPosition: Seat): List<Int> {
return when(seatPosition) {
Seat.F, Seat.L -> {
listOfSeat.windowed(listOfSeat.count() / 2).first()
}
Seat.B, Seat.R -> {
listOfSeat.windowed(listOfSeat.count() / 2).last()
}
}
}
enum class Seat(seat: Char) {
F('F'),
B('B'),
L('L'),
R('R')
}
Either you mutate the variable:
fun main(args: Array<String>) {
var listOfSeatRows = (1..127).toList()
// Just loop until all the listOfPass has completed.
listOfPass.forEach { seatPass ->
val seat = Seat.valueOf(seatPass.toString())
// I want to pass in the new updated list not the same list
listOfSeatRows = getListOfSeatRows(listOfSeatRows, seat)
}
}
or you mutate the list:
fun main(args: Array<String>) {
var listOfSeatRows = (1..127).toMutableList()
// Just loop until all the listOfPass has completed.
listOfPass.forEach { seatPass ->
val seat = Seat.valueOf(seatPass.toString())
// I want to pass in the new updated list not the same list
reduceListOfSeatRows(listOfSeatRows, seat)
}
}
private fun reduceListOfSeatRows(listOfSeat: MutableList<Int>, seatPosition: Seat) {
val half = listOfSeat.size / 2
when(seatPosition) {
Seat.F, Seat.L -> {
while (listOfSeat.size > half) listOfSeat.removeLast()
}
Seat.B, Seat.R -> {
while (listOfSeat.size > half) listOfSeat.removeFirst()
}
}
}
If you stick with mutating the property, your function can be simplified (and avoid wasteful creation of multiple intermediate lists) using take/takeLast:
private fun getListOfSeatRows(listOfSeat: List<Int>, seatPosition: Seat): List<Int> {
return when(seatPosition) {
Seat.F, Seat.L -> {
listOfSeat.take(listOfSeat.size / 2)
}
Seat.B, Seat.R -> {
listOfSeat.takeLast(listOfSeat.size / 2)
}
}
}
recursion
maybe that's will help with some enhancement depending on your code:
var ss = 1
val listOfPass = listOf<Char>('F', 'L','B','R')
fun main(args: Array<String>) {
val listOfSeatRows = (1..127).toList()
val answer = getListOfSeatRows(
listOfSeatRows,
listOfSeatRows.count() / 2,
Seat.valueOf(listOfPass[0].toString())
)
println(answer)
}
private fun getListOfSeatRows(listOfSeat: List<Int>, count: Int, seatPosition: Seat): List<Int> {
val tempList: List<Int> = when (seatPosition) {
Seat.F, Seat.L -> {
listOfSeat.windowed(count).first()
}
Seat.B, Seat.R -> {
listOfSeat.windowed(count).last()
}
else -> listOfSeat
}
if(count == 0 || count == 1) return listOfSeat
if (listOfPass.size > ss) {
val seat = Seat.valueOf(listOfPass[ss++].toString())
return getListOfSeatRows(tempList, count / 2, seat)
}
return listOfSeat
}

BillingClient.querySkuDetails(SkuDetailsParams.builder()) not working?

Getting an empty list when fetching with querySkuDetails()?
So in case you've been having this issue lately, where you want to fetch your Google Play Console list of SkuDetail, maybe to show the price of one of the SkuDetail and show it to the user has it was in my case or to display some other information about a SkuDetail from your Google Play Console merchant account. Anyways, here's what's worked for me:
First you need to add this to your build.gradle app file:
implementation "com.android.billingclient:billing-ktx:4.0.0"
Then Inside of my Fragment's ViewModel, I did the following:
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val billingClient by lazy {
BillingClient.newBuilder(application.applicationContext)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build()
}
/**
#param result Returns true if connection was successful, false if otherwise
*/
private inline fun billingStartConnection(crossinline result: (Boolean) -> Unit) {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
result(true)
}
}
override fun onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
result(false)
}
})
}
sealed class BillingClientObserver {
object Loading : BillingClientObserver()
object ClientDisconnected : BillingClientObserver()
object HasNoPurchases : BillingClientObserver()
object HasNoAdsPrivilege : BillingClientObserver()
object UserCancelledPurchase : BillingClientObserver()
data class UnexpectedError(val debugMessage: String = "") : BillingClientObserver()
}
private val _billingClientObserver: MutableStateFlow<BillingClientObserver> =
MutableStateFlow(BillingClientObserver.Loading)
val billingClientObserver: StateFlow<BillingClientObserver> = _billingClientObserver
suspend fun checkSkuDetailById(productId: String) =
billingStartConnection { billingClientReady ->
if (billingClientReady) {
val skuList = ArrayList<String>()
skuList.add(productId)
val params = SkuDetailsParams.newBuilder()
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP)
viewModelScope.launch(Dispatchers.Main) {
val skuDetailList = withContext(Dispatchers.IO) {
billingClient.querySkuDetails(params.build())
}
skuDetailList.skuDetailsList?.let {
Timber.d("Timber> List<SkuDetails>: $it")
if (it.isNotEmpty()) {
val skuDetails: SkuDetails = it[0]
_goAdsFreePricing.value = skuDetails.price
} else {
_billingClientObserver.value =
BillingClientObserver.UnexpectedError(context.getString(R.string.unable_to_get_price_msg))
}
} ?: run {
_billingClientObserver.value =
BillingClientObserver.UnexpectedError(context.getString(R.string.unable_to_get_price_msg))
}
}
} else {
_billingClientObserver.value =
BillingClientObserver.UnexpectedError(context.getString(R.string.unable_connect_to_play_store))
}
}
}
The most important thing to do for the
BillingClient.querySkuDetails(SkuDetailsParams.builder())
to be successful, is to first establish a successful connection through the
BillingClient.startConnection( listener: BillingClientStateListener)
then do the query on the background thread, very important that the
querySkuDetails()
happens in the background thread as it is made to fail if done on the
#MainThread
then listen for its result on the
#MainThread
like in the above example.

mockk, how to use slot for MutableMap<String, String>

using mockk 1.9.3
having a function to be verified
class EventLogger private constructor()
fun logUserEvent(eventName: String?, eventParamMap: MutableMap<String, String>?) {
......
internaLogEventImpl(eventName, eventParamMap)
}
internal fun internaLogEventImpl(eventName: String?, customParams: MutableMap<String, String>?) {
......
}
companion object {
#Volatile
private var sEventLoggerSingleton: EventLogger? = null
#JvmStatic
val instance: EventLogger
get() {
if (sEventLoggerSingleton == null) {
sEventLoggerSingleton = EventLogger()
}
return sEventLoggerSingleton!!
}
}
got compiler error at every {eventLogger.internaLogEventImpl(any(), mapSlot)}
Type mismatch.
Required: MutableMap<String, String>?
Found: CapturingSlot<MutableMap<String, String>>
when trying this below :
class TestK {
lateinit var eventLogger: EventLogger
lateinit var application: Application
val mapSlot = slot<MutableMap<String, String>>()
#Before
fun setUp() {
application = ApplicationProvider.getApplicationContext<Application>()
eventLogger = mockk.spyk(EventLogger.instance)
ReflectionHelpers.setStaticField(EventLogger::class.java, "sEventLoggerSingleton", eventLogger)
}
#After
fun cleanUp() {
ReflectionHelpers.setStaticField(EventLogger::class.java, "sEventLoggerSingleton", null)
}
#Test
fun logNotificationStatusChange_with_enabled_WhenCalled_ShouldLog() {
val testMap = hashMapOf("action" to "open")
every {eventLogger.internaLogEventImpl(any(), mapSlot)} answers {
println(mapSlot.captured)
assert(mapSlot.captured["action"] == "open")
}
eventLogger.logUserEvent("test_event", testMap)
}
}
You need to use capture (see Mockk's Capturing section).
So for your case capture(mapSlot) should work.
eventLogger.internaLogEventImpl(any(), capture(mapSlot))
Before trying to mock on complex code, It would be better to learn on easier example.
Here is a working example to mock a private call on an object with mockk.
object MyLogger {
fun logUserEvent(event: String?, map: MutableMap<String, String>?) {
// turns the event string into an uppercase string.
internaLogEventImpl(event?.toUpperCase(), map)
}
private fun internaLogEventImpl(event: String?, map: MutableMap<String, String>?): Unit =
throw Exception("real implementation")
}
How to test and mock the internal function so we don't throw the exception.
#Test
fun `test logger internal`() {
val expectedMap = mutableMapOf("a" to "b")
val expectedEvent = "EVENT"
val mock = spyk(MyLogger, recordPrivateCalls = true)
justRun { mock["internaLogEventImpl"](expectedEvent, expectedMap) }
// or justRun { mock["internaLogEventImpl"](any<String>(), any<MutableMap<String, String>>()) }
mock.logUserEvent("event", expectedMap)
verify { mock["internaLogEventImpl"](expectedEvent, expectedMap) }
}
Here logUserEvent calls the real implementation and internaLogEventImpl calls the mock implementation.
if justRun { mock["internaLogEventImpl"](expectedEvent, expectedMap) } is not called (or wrong because the argument doesn't match), the real implementation will be call. Here it will throw Exception("real implementation").
Please try and modify values to check different behaviors.

LiveData unit test do not pass under coroutine and multithread because of return true instead of expected false

EnrollmentViewModelTest
#Test
fun getUserProfile() {
val responseSnapshot = this.javaClass.getResource("/userDetailResponse.json").readText()
val user = Gson().fromJson<User>(responseSnapshot, User::class.java)
val response = Response.success(user)
val deferred = CompletableDeferred<Response<User>>(response)
coEvery { userService.getUserDetail() } returns deferred
viewModel.getUserProfile()
assert(viewModel.loadingStatus.value != null)
assert(!UITestUtil.getValue(viewModel.loadingStatus)!!)
assertEquals(false, viewModel.loadingStatus.value!!)
}
Here is EnrollmentViewModel.kt
fun getUserProfile() {
loadingStatus.postValue(true)
job = launch {
callAsync {
userService.getUserDetail()
}.onSuccess { user ->
if (user != null) {
processUserDetails(user)
}
loadingStatus.postValue(false)
}.onError {
//
}.onException {
//
}
}
}
When I debug the test case, it show that the UITestUtil.getValue(viewModel.loadingStatus)!! is false.
But weirdly, the test case does not pass, and when I print the UITestUtil.getValue(viewModel.loadingStatus)!!. It is true.
It may related to the loadingStatus.postValue(true)
After I remove it, the print result is false.
But I do not know why.
object UITestUtil {
/**
* Gets the value of a LiveData safely.
*/
#Throws(InterruptedException::class)
fun <T> getValue(liveData: LiveData<T>): T? {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
liveData.removeObserver(this)
}
}
liveData.observeForever(observer)
latch.await(2, TimeUnit.SECONDS)
return data
}
}
Updated:
import com.google.gson.Gson
import kotlinx.coroutines.Deferred
import retrofit2.Response
import retrofit2.Response.success
data class VPlusResult<T : Any?>(
val response: Response<T>? = null,
val exception: Exception? = null
)
inline fun <T : Any> VPlusResult<T>.onSuccess(action: (T?) -> Unit): VPlusResult<T> {
if (response?.isSuccessful == true)
action(response.body())
return this
}
inline fun <T : Any> VPlusResult<T>.onRawSuccess(action: (response: Response<T>) -> Unit): VPlusResult<T> {
if (response?.isSuccessful == true)
action(response)
return this
}
inline fun <T : Any, TR : Any> VPlusResult<T>.map(action: (T?) -> (TR)) =
if (response?.isSuccessful == true) VPlusResult(success(action(response.body())))
else this as VPlusResult<TR>
inline fun <T : Any> VPlusResult<T>.onError(action: (String) -> Unit): VPlusResult<T> {
if (response?.isSuccessful != true) {
response?.errorBody()?.let {
action(it.string())
}
}
return this
}
inline fun <T : Any, reified G : Any> VPlusResult<T>.onErrorJson(action: (G) -> Unit): VPlusResult<T> {
if (response?.isSuccessful != true) {
response?.errorBody()?.let {
action(Gson().fromJson(it.string(), G::class.java))
}
}
return this
}
inline fun <T : Any> VPlusResult<T>.onRawError(action: (Response<T>?) -> Unit): VPlusResult<T> {
if (response?.isSuccessful != true) {
action(response)
}
return this
}
inline fun <T : Any?> VPlusResult<T>.onException(action: (Exception) -> Unit) {
exception?.let { action(it) }
}
inline fun <T : Any> VPlusResult<T>.onSadness(action: (String?) -> Unit): VPlusResult<T> {
onError {
action(it)
}.onException {
action(it.message)
}
return this
}
suspend fun <T : Any> callAsync(block: () -> Deferred<Response<T>>): VPlusResult<T> {
return try {
VPlusResult(block().await())
} catch (e: Exception) {
VPlusResult(exception = e)
}
}
Updated 2:
adding either of the following statements before assert statement will get the value false, which passes the test case.
coVerify { userService.getUserDetail() }
coVerify { viewModel.processUserDetails(user) }
Update 3:
fun loadAllTasksFromRepository_loadingTogglesAndDataLoaded()
in https://github.com/android/architecture-samples
Also works, but I try to introduce into my codes, it also failed.
#ExperimentalCoroutinesApi
#Test
fun getUserProfile4Using() {
// using TestCoroutineScope in kotlinx.coroutines.test
// Pause dispatcher so we can verify initial values
mainCoroutineRule.pauseDispatcher()
val responseSnapshot = this.javaClass.getResource("/userDetailResponse.json").readText()
val user = Gson().fromJson<User>(responseSnapshot, User::class.java)
val response = Response.success(user)
val deferred = CompletableDeferred<Response<User>>(response)
// coEvery { userService.getUserDetail() } returns deferred
viewModel.getUserProfile()
assert(viewModel.loadingStatus.value != null)
// verify { viewModel.processUserDetails(user) }
print(viewModel.loadingStatus.value)
assert(UITestUtil.getValue(viewModel.loadingStatus)!!)
assertEquals(true, viewModel.loadingStatus.value!!)
// Execute pending coroutines actions
mainCoroutineRule.resumeDispatcher()
print(viewModel.loadingStatus.value!!)
assert(!UITestUtil.getValue(viewModel.loadingStatus)!!)
assertEquals(false, viewModel.loadingStatus.value!!)
}
I'd say you should probably rewrite your test using runBlockingTest(https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/)
fun getUserProfile() = runBlockingTest{
val responseSnapshot = this.javaClass.getResource("/userDetailResponse.json").readText()
val user = Gson().fromJson<User>(responseSnapshot, User::class.java)
val response = Response.success(user)
val deferred = CompletableDeferred<Response<User>>(response)
coEvery { userService.getUserDetail() } returns deferred
viewModel.getUserProfile()
assert(viewModel.loadingStatus.value != null)
assert(!UITestUtil.getValue(viewModel.loadingStatus)!!)
assertEquals(false, viewModel.loadingStatus.value!!)
}
You won't have to use those countdownlatches etc. This approach has literally fixed all of my issues with testing coroutines, hope it helps you as well :)! If not, just let me know.

When unit testing my ViewModel, list always returns empty

I'm trying to learn unit testing, I have my code factored into an MVVM(i) architecture, but when I run my testParseToList() test function, it always comes back with an empty list and I can't figure out why. I fear it may have something to do with the i part of the MVVM(i) and whether or not I'm correctly mocking my viewmodel. I'm starting with my most simple viewmodel in hopes to get a grasp of the concepts before moving onto my more complex ones.
OfflineViewModelUnitTest.kt
#RunWith(JUnit4::class)
class OfflineViewModelUnitTest {
#get:Rule
val rule = InstantTaskExecutorRule()
#Mock
var offlineViewModel: OfflineViewModel = OfflineViewModel(OfflineInteractorImpl())
#Before
fun setup() {
MockitoAnnotations.initMocks(this)
DaggerOfflineViewModelComponent.builder()
.offlineInteractorImplModule(OfflineInteractorImplModule())
.build()
.inject(offlineViewModel)
// this.offlineViewModel = OfflineViewModel(OfflineInteractorImpl())
}
#Test
fun testParseToList() {
val test = offlineViewModel.parseTextToList("dried bonito extract,\n" +
" ketchup,\n" +
" millet,\n" +
" corn & wheat protein")
val a = "dried bonito extract"
val b = "ketchup"
val c = "millet"
val d = "corn & wheat protein"
val expectedResult = listOf(a, b, c, d)
assertEquals(expectedResult, test)
}
}
OfflineViewModel.kt
class OfflineViewModel(private val offlineInteractor: OfflineInteractor): ViewModel() {
init {
DaggerOfflineViewModelComponent.builder()
.offlineInteractorImplModule(OfflineInteractorImplModule())
.build()
.inject(this)
}
fun parseTextToList(firebaseVisionTextString: String): MutableList<String> {
Log.d("here it is", firebaseVisionTextString)
return offlineInteractor.parseTextToList(firebaseVisionTextString)
}
fun readCsvFromAssetFolder(inputStream: InputStream): List<String>{
return offlineInteractor.readCsvFromAssetFolder(inputStream)
}
}
OfflineInteractorImpl.kt
class OfflineInteractorImpl: OfflineInteractor {
override fun parseTextToList(firebaseVisionTextString: String): MutableList<String> {
val ingredientsList: MutableList<String> = firebaseVisionTextString.split(",").map { it.trim() }.toMutableList()
return ingredientsList
}
override fun readCsvFromAssetFolder(inputStream: InputStream): List<String> {
val csvLine: ArrayList<String> = ArrayList()
var content: Array<String>?
try
{
val br = BufferedReader(InputStreamReader(inputStream))
for (line in br.lines())
{
content = line.split((",").toRegex()).dropLastWhile{ it.isEmpty() }.toTypedArray()
csvLine.add(content[0].substringBefore(";"))
}
br.close()
}
catch (e: IOException) {
e.printStackTrace()
}
return csvLine
}
}
Test Results
java.lang.AssertionError:
Expected :[dried bonito extract, ketchup, millet, corn & wheat protein]
Actual :[]
Like second said, since you mocked offlineViewModel it is going to return an empty string, unless you define something for it to return using when().
Source: https://github.com/mockito/mockito/wiki/FAQ#what-values-do-mocks-return-by-default