Generic type in MutableLiveData in kotlin - android

I have a val _user: MutableLiveData<Resource<List<ApiUser>>> = MutableLiveData()in viewmodel but i want to postValue errorException
// A generic class that contains data and status about loading this data.
sealed class Resource<T>(
val data: T? = null,
val message: String? = null
) {
class Success<T>(data: T) : Resource<T>(data)
class Loading<T>(data: T? = null) : Resource<T>(data)
class Error<T>(message: String, data: T? = null) : Resource<T>(data, message)
}
//ViewModel
class HomeViewModel : ViewModel() {
val _user: MutableLiveData<Resource<List<ApiUser>>> = MutableLiveData()
var job: CompletableJob? = null
fun f() {
job = Job()
_user.postValue(Resource.Loading(null))
CoroutineScope(IO+job!!).launch {
try {
_user.postValue(Resource.Success(RetrofitBuilder.apiService.getUsers()))
} catch (e: Throwable) {
_user.postValue(Resource.Error("",e))
}
}
}
fun cancelJob() {
job?.cancel()
}
}
//Fragment
fun subScribeUI() {
viewModel!!._user.observe(viewLifecycleOwner, Observer {
it?.let {
when(it.status) {
Status.LOADING -> {
Timber.d("LOADING")
}
Status.SUCCESS -> {
Timber.d("SUCCESS")
}
Status.ERROR -> {
Timber.d("ERROR")
}
}
}
})
}
override fun onDestroyView() {
super.onDestroyView()
viewModel?.cancelJob()
}

The problem is that you are trying to assign e, which is of type Throwable to argument of type List<ApiUser>.
Instead of
_user.postValue(Resource.Error("", e))
you need to do this:
_user.postValue(Resource.Error(errorMessage, emptyList())
or
_user.postValue(Resource.Error(errorMessage, null)
where errorMessage is e.message or something similar.

Related

NullPointerException with adapter

I have this observer for my viewmodel so that I can setup my adapter but, when I run the app, it gives me the NullPointerException error on this line:
japaneseAdapter = it.data?.let { it1 -> JapaneseAdapter(it1) }!!
This is the activity with that line:
#AndroidEntryPoint
class JapaneseActivity : AppCompatActivity() {
private lateinit var binding: ActivityJapaneseBinding
private val japaneseViewModel by viewModels<JapaneseViewModel>()
private lateinit var japaneseAdapter: JapaneseAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityJapaneseBinding.inflate(layoutInflater)
setContentView(binding.root)
japaneseViewModel.japaneseResponse.observe(this, {
when(it.status){
Resource.Status.LOADING -> { }
Resource.Status.SUCCESS -> {
japaneseAdapter = it.data?.let { it1 -> JapaneseAdapter(it1) }!!
binding.rvNews.adapter = japaneseAdapter
}
Resource.Status.ERROR -> { Log.d("ERROR","ERROR RAISED") }
}
})
}
}
This is the adapter:
class JapaneseAdapter(private var japaneseResponse: List<JapaneseResponse>) :
RecyclerView.Adapter<JapaneseAdapter.ViewHolder>() {
inner class ViewHolder(
view: View
) : RecyclerView.ViewHolder(view) {
private val binding = NewsItemsBinding.bind(view)
private val itemTitle: TextView = binding.tvTitle
private val itemImage: ImageView = binding.ivNews
private val itemDescription: TextView = binding.tvDescription
fun bind(response: JapaneseResponse) {
Picasso.get().load(response.urlToImage).into(itemImage)
itemTitle.text = response.Title
itemDescription.text = response.Description
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.news_items, parent, false)
return ViewHolder(v)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(japaneseResponse[position])
}
override fun getItemCount(): Int {
return japaneseResponse.size
}
}
Generic data source:
abstract class BaseDataSource {
protected suspend fun <T> getResult(call: suspend () -> Response<ApiResponse<T>>): Resource<T> {
try {
val response = call()
// if(response.isSuccessful) {
// val body = response.body()?.data
// if(body != null) return Resource.success(body)
// }
val body = response.body()?.data
return Resource.success(body)
//return Resource.error("${response.code()}: ${response.message()}")
} catch (e: Exception) {
return Resource.error(e.message ?: "Generic error")
}
}
}
data class Resource<out T>(val status: Status, val data: T?, val message: String?) : Serializable {
enum class Status {
SUCCESS,
ERROR,
LOADING
}
companion object {
fun <T> success(data: T?): Resource<T> {
return Resource(
Status.SUCCESS,
data,
null
)
}
fun <T> error(message: String, data: T? = null): Resource<T> {
return Resource(
Status.ERROR,
data,
message
)
}
fun <T> loading(data: T? = null): Resource<T> {
return Resource(
Status.LOADING,
data,
null
)
}
}
fun isSuccessful() = status == Status.SUCCESS
fun isError() = status == Status.ERROR
fun isLoading() = status == Status.LOADING
}
The data source for the Japanese news:
class JapaneseDataSource #Inject constructor(private val japaneseService: JapaneseService) :
BaseDataSource() {
suspend fun getJpNews() = getResult { japaneseService.jpNews() }
}
Repository:
class JapaneseRepository #Inject constructor(
private val remote: JapaneseDataSource
) {
suspend fun jpNews() =
remote.getJpNews()
}
The service:
interface JapaneseService {
#GET("/v2/top-headlines?country=jp&apiKey=77acc490875643c5b2328fb615e0cf83")
suspend fun jpNews(): Response<ApiResponse<List<JapaneseResponse>>>
}
I can see that the response is there since I have okhttp logging it for me but for some reason it seems to be null and I am not sure why...
Any help?
japaneseAdapter = it.data?.let { it1 -> JapaneseAdapter(it1) }!!
!! is Kotlin's "crash the app" operator. It says that you want to crash the app if the value you are applying !! to is null. Your objective, as a Kotlin programmer, is to avoid using !!.
In this case, the fact that you are crashing on that line with that error means that !! is being applied to null. That will occur if it.data evaluates to null.
it.data appears to be a Resource object with a status of SUCCESS. So, presumably, you are calling success() with a value of null.
With all that in mind, you will need to use your debugger and see why val body = response.body()?.data is evaluating to null, or see where else you are getting a Resource with null data.
And, please, try to avoid using !!.

How to update RecyclerView when use retrofit?

i want to update RecyclerView when item removed but i dont' have idea to solve, i try to use notifydatasetchanged() but it don't work.
API Interface
interface RetrofitService {
#GET("/")
suspend fun getAllItem() : Response<List<Item>>
#POST("/delete")
suspend fun deleteItem(#Body item :Item) : Response<Item>
class Repository
class ItemRepository(private val retrofitService: RetrofitService) {
suspend fun getAllItem() = retrofitService.getAllItem()
suspend fun deleteItem(item : Item) = retrofitService.deleteItem(item)
}
in ViewModel, i handle result by two function(handleItemResponse and handleListItemResponse)
class ItemViewModel(private val itemRepository: ItemRepository) : ViewModel() {
val itemResponse: MutableLiveData<Resource<Item>> = MutableLiveData()
val listItemResponse : MutableLiveData<Resource<List<Item>>> = MutableLiveData()
fun getItemResponse() = viewModelScope.launch {
val response : Response<List<Item>> = itemRepository.getAllItem()
withContext(Dispatchers.Main) {
listItemResponse.postValue(handleListItemResponse(response))
}
}
fun deleteItem(item: Item) = viewModelScope.launch {
val response = itemRepository.deleteItem(item)
itemResponse.postValue(handleItemResponse(response))
}
private fun handleItemResponse(response: Response<Item>): Resource<Item> {
if (response.isSuccessful) {
response.body()?.let { resultResponse ->
return Resource.Success(resultResponse)
}
}
return Resource.Error(response.message())
}
private fun handleListItemResponse(response: Response<List<Item>>): Resource<List<Item>> {
if (response.isSuccessful) {
response.body()?.let { resultResponse ->
return Resource.Success(resultResponse)
}
}
return Resource.Error(response.message())
}
}
and class Resource to logging result
sealed class Resource<T>(
val data:T?=null,
val messaage : String? = null
) {
class Success<T>(data:T) : Resource<T>(data)
class Error<T>(messaage: String,data:T? = null) : Resource<T>(data,messaage)
class Loading<T> : Resource<T>()
}
in Fragment , i use ViewModel like this
viewModel.listItemResponse.observe(viewLifecycleOwner, Observer {
when (it) {
is Resource.Success -> {
Toast.makeText(context,"All Success",Toast.LENGTH_SHORT).show()
it.data?.let { itemReponse ->
listItem = itemReponse
adapter.setNotes(listItem)
}
}
is Resource.Error -> {
it.messaage?.let { msg ->
Log.e("AAA","ERR:$msg ")
}
}
}
})
viewModel.getItemResponse()
}
in Adapter , i getList by function setNotes
fun setNotes(items:List<Item>){
this.items = items
notifyDataSetChanged()
}
and function to delete item
private val onItemDelete:(Item)->Unit ={ item ->
viewModel.itemResponse.observe(viewLifecycleOwner, Observer {
when (it) {
is Resource.Success -> {
it.data?.let {
Toast.makeText(context,"Delete successfully!",Toast.LENGTH_SHORT).show()
}
}
is Resource.Error -> {
it.messaage?.let { msg ->
Toast.makeText(context," \"ERR:$msg \"",Toast.LENGTH_SHORT).show()
}
}
}
})
viewModel.deleteItem(item)
}
My english not well , so i hope you sympathize and help me, have a nice day,everyone!
I fixed it, i put it for anyone face same my problem.
private val onItemDelete:(Int)->Unit ={ pos ->
viewModel.itemResponse.observe(viewLifecycleOwner, Observer {
when (it) {
is Resource.Success -> {
it.data?.let {
listItem.removeAt(pos)
adapter.notifyItemRemoved(pos)
}
}
is Resource.Error -> {
it.messaage?.let { msg ->
Toast.makeText(context," \"ERR:$msg \"",Toast.LENGTH_SHORT).show()
}
}
}
})
viewModel.deleteItem(listItem[pos])
}
just callback position of item then get it in fragment.

Capture suspend lambda parameter with generic result in a mockked class

I want to know if it is possible to capture an suspend lambda with generic result with MockK and JUnit5.
I've tried several ways, and in the most recent I'm having a KotlinNullPointerException when I try to run the test.
Here is the code, with the complete class dependencies and test:
class Test {
data class Result<out T>(val status: ResultStatus, val data: T?, val exception: Exception?) {
companion object {
fun <T> success(data: T?): Result<T> {
return Result(ResultStatus.SUCCESS, data, null)
}
fun <T> error(exception: Exception): Result<T> {
return Result(ResultStatus.ERROR, null, exception)
}
fun <T> loading(data: T? = null): Result<T> {
return Result(ResultStatus.LOADING, data, null)
}
}
}
class Dep1() {
fun <R> methodToMock(viewModelScope: CoroutineScope, block: suspend CoroutineScope.() -> R): MutableLiveData<Result<R>> {
val result = MutableLiveData<Result<R>>()
result.value = Result.loading()
viewModelScope.launch {
try {
var asyncRequestResult: R? = null
withContext(Dispatchers.Default) {
asyncRequestResult = block()
}
result.value = Result.success(asyncRequestResult)
} catch (cancellationException: CancellationException) {
} catch (exception: Exception) {
result.value = Result.error(exception)
}
}
return result
}
}
class Dep2() {
fun methodToAssert(): Boolean {
println("Called")
return true
}
}
class ClassToTest(private val dep1: Dep1, private val dep2: Dep2) {
fun methodToCall(coroutineScope: CoroutineScope): MutableLiveData<Result<Boolean>> {
return dep1.methodToMock(coroutineScope) {
dep2.methodToAssert()
}
}
}
private val dep1: Dep1 = mockk()
private val dep2: Dep2 = mockk(relaxed = true)
private val mViewModelScope: CoroutineScope = GlobalScope
#Test
fun `Check if is calling the required methods correctly`() {
val classToTest = ClassToTest(dep1, dep2)
val transactionLambda = slot<suspend CoroutineScope.() -> Boolean>()
coEvery { dep1.methodToMock(mViewModelScope, capture(transactionLambda)) } coAnswers {
MutableLiveData(Result.success(transactionLambda.captured.invoke(mViewModelScope)))
}
classToTest.methodToCall(mViewModelScope)
verify { dep2.methodToAssert() }
}
}
If anyone is also having this problem, I was able to solve it using every instead of coEvery and calling the coInvoke() slot method:
every { dep1.methodToMock(mViewModelScope, capture(transactionLambda)) } answers {
MutableLiveData(Result.success(transactionLambda.coInvoke(mViewModelScope)))
}

How can I convert LiveData to 2 pieces of liveData?

I have configLiveData:LiveData<Response<ConfigFile>> where Response is
sealed class Response<out T> {
data class Success<out T>(val data: T) : Response<T>()
data class Failure<out T>(val message: String) : Response<T>()
}
now in ViewModel I want to transform configLiveData to two different
LiveDatas 1.LiveData<ConfigFile> and 2. LiveData<String> and as a result of transformation one of them will be empty.
but I want to get ride of LiveData<Response<ConfigFile>> and have instead of it LiveData<ConfigFile>
override suspend fun fetchConfigFile(): Response<ConfigFile> {
return suspendCoroutine { cont ->
EnigmaRiverContext.getHttpHandler().doHttp(AppConstants.getPath().append("config/appConfig.json").toURL(),
JsonHttpCall("GET"), object : JsonReaderResponseHandler() {
override fun onSuccess(jsonReader: JsonReader) {
try {
val successObject = ApiConfigFile(jsonReader)
cont.resume(Response.Success(successObject.toPresenterModel()))
} catch (e: IOException) {
cont.resume(Response.Failure(e.message))
} catch (e: Exception) {
cont.resume(Response.Failure(e.message ))
}
}
override fun onError(error: Error?) {
cont.resume(Response.Failure("error.message"))
}
})
}
}
It is how my Repository looks like
private fun fetchConfig() {
uiScope.launch {
when (val result = homeRepository.fetchConfigFile()) {
is Response.Success<ConfigFile> -> {
postValue(Response.Success(result.data))
}
is Response.Failure -> postValue(Response.Failure(result.message))
}
}
}
class ConfigFileLiveData #Inject constructor(val homeRepository: IHomeRepository) : LiveData<Response<ConfigFile>>() {
private val liveDataJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + liveDataJob)
override fun onActive() {
fetchConfig()
}
private fun fetchConfig() {
viewModelScope.launch {
when (val result = homeRepository.fetchConfigFile()) {
is Response.Success<ConfigFile> -> {
postValue(Response.Success(result.data))
}
is Response.Failure -> postValue(Response.Failure(result.message))
}
}
}
}
I have `ConfigFileLiveData` which is singleton and I want to use this liveData in other viewModels as I need to fetch config once and use it in different ViewModels
class ConfigFileLiveData #Inject constructor(val homeRepository: IHomeRepository) : LiveData<Response<ConfigFile>>() {
override fun onActive() {
fetchConfig()
}
private fun fetchConfig() {
viewModelScope.launch {
when (val result = homeRepository.fetchConfigFile()) {
is Response.Success<ConfigFile> -> {
postValue(Response.Success(result.data))
}
is Response.Failure -> postValue(Response.Failure(result.message))
}
}
}
}
In Viewmodel Define two LiveData variable.
private var configLiveData = MutableLiveData<ConfigFile>()
private var stringLiveData = MutableLiveData<String>()
Modify this method
private fun fetchConfig() {
uiScope.launch {
when (val result = homeRepository.fetchConfigFile()) {
is Response.Success<ConfigFile> -> {
configLiveData.value = Response.Success(result.data)
}
is Response.Failure -> {
stringLiveData.value = Response.Failure(result.message)
}
}
}
}

Android ViewState using RxJava or kotlin coroutines

I'm trying to learn how to use RxJava in Android, but have run into a dead end. I have the following DataSource:
object DataSource {
enum class FetchStyle {
FETCH_SUCCESS,
FETCH_EMPTY,
FETCH_ERROR
}
var relay: BehaviorRelay<FetchStyle> = BehaviorRelay.createDefault(FetchStyle.FETCH_ERROR)
fun fetchData(): Observable<DataModel> {
return relay
.map { f -> loadData(f) }
}
private fun loadData(f: FetchStyle): DataModel {
Thread.sleep(5000)
return when (f) {
FetchStyle.FETCH_SUCCESS -> DataModel("Data Loaded")
FetchStyle.FETCH_EMPTY -> DataModel(null)
FetchStyle.FETCH_ERROR -> throw IllegalStateException("Error Fetching")
}
}
}
I want to trigger an update downstream, whenever I change the value of relay, but this doesn't happen. It works when the Activity is initialized, but not when I'm updating the value. Here's my ViewModel, from where I update the value:
class MainViewModel : ViewModel() {
val fetcher: Observable<UiStateModel> = DataSource.fetchData().replay(1).autoConnect()
.map { result -> UiStateModel.from(result) }
.onErrorReturn { exception -> UiStateModel.Error(exception) }
.startWith(UiStateModel.Loading())
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
fun loadSuccess() {
DataSource.relay.accept(DataSource.FetchStyle.FETCH_SUCCESS)
}
fun loadEmpty() {
DataSource.relay.accept(DataSource.FetchStyle.FETCH_EMPTY)
}
fun loadError() {
DataSource.relay.accept(DataSource.FetchStyle.FETCH_ERROR)
}
}
This is the code from the Activity that does the subsciption:
model.fetcher
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
uiState -> mainPresenter.loadView(uiState)
})
Ended up using kotlin coroutines instead, as I was unable to re-subscribe to ConnectableObservable and start a new fetch.
Here's the code for anyone interested.
The presenter:
class MainPresenter(val view: MainView) {
private lateinit var subscription: SubscriptionReceiveChannel<UiStateModel>
fun loadSuccess(model: MainViewModel) {
model.loadStyle(DataSource.FetchStyle.FETCH_SUCCESS)
}
fun loadError(model: MainViewModel) {
model.loadStyle(DataSource.FetchStyle.FETCH_ERROR)
}
fun loadEmpty(model: MainViewModel) {
model.loadStyle(DataSource.FetchStyle.FETCH_EMPTY)
}
suspend fun subscribe(model: MainViewModel) {
subscription = model.connect()
subscription.subscribe { 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()
}
}
}
fun unSubscribe() {
subscription.close()
}
}
inline suspend fun <E> SubscriptionReceiveChannel<E>.subscribe(action: (E) -> Unit) = consumeEach { action(it) }
The view:
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
launch(UI) {
mainPresenter.subscribe(model)
}
btn_load_success.setOnClickListener {
mainPresenter.loadSuccess(model)
}
btn_load_error.setOnClickListener {
mainPresenter.loadError(model)
}
btn_load_empty.setOnClickListener {
mainPresenter.loadEmpty(model)
}
}
override fun onDestroy() {
super.onDestroy()
Log.d("View", "onDestroy()")
mainPresenter.unSubscribe()
}
...
The model:
class MainViewModel : ViewModel() {
val TAG = this.javaClass.simpleName
private val stateChangeChannel = ConflatedBroadcastChannel<UiStateModel>()
init {
/** When the model is initialized we immediately start fetching data */
fetchData()
}
override fun onCleared() {
super.onCleared()
Log.d(TAG, "onCleared() called")
stateChangeChannel.close()
}
fun connect(): SubscriptionReceiveChannel<UiStateModel> {
return stateChangeChannel.openSubscription()
}
fun fetchData() = async {
stateChangeChannel.send(UiStateModel.Loading())
try {
val state = DataSource.loadData().await()
stateChangeChannel.send(UiStateModel.from(state))
} catch (e: Exception) {
Log.e("MainModel", "Exception happened when sending new state to channel: ${e.cause}")
}
}
internal fun loadStyle(style: DataSource.FetchStyle) {
DataSource.style = style
fetchData()
}
}
And here's a link to the project on github.

Categories

Resources