I am trying to inject the below ViewModel into my fragment using koin.
package com.example.koinapplication.ui.main
import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.koinapplication.models.Dog
import com.example.koinapplication.repo.DataRepo
import com.example.koinapplication.repo.DataSource
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.java.KoinJavaComponent.inject
class MainViewModel : ViewModel() {
private val dataRepo by inject(DataSource::class.java)
private var limit = 10
private val _dogListLiveData = MutableLiveData<List<Dog>>()
private var dogList = mutableListOf<Dog>()
val dogListLiveData: MutableLiveData<List<Dog>>
get() = _dogListLiveData
fun searchByBreed(queryText: String) {
dataRepo.searchByBreed(
queryText,
object : DataSource.OnResponseCallback<List<Dog>, String> {
override fun onSuccess(obj: List<Dog>?) {
dogList = mutableListOf()
if (!obj.isNullOrEmpty()) {
dogList.addAll(obj)
dogListLiveData.value = dogList.take(limit)
}
}
override fun onError(error: String) {
Log.i("Calling Network Service", error)
}
})
}
fun loadPaginateBreed(): Boolean {
return if ((limit + 10) < dogList.size) {
limit += 10
Log.i("Pagination new Limit", limit.toString())
dogListLiveData.value = dogList.take(limit)
false
} else {
limit += dogList.size % limit
dogListLiveData.value = dogList.take(limit)
true
}
}
}
these are my modules
#OptIn(KoinApiExtension::class)
val networkModule = module{
factory { AuthInterceptor() }
factory { provideOkHttpClient(get()) }
factory { GranularErrorCallAdapterFactory<Any>() }
single { providesNetworkClient(get(), get()) }
single<DataSource> { DataRepo(get()) }
single <DataSource> { NetworkRepo(get()) }
}
#OptIn(KoinApiExtension::class)
val viewModelModule = module(override = true) {
viewModel { MainViewModel() }
}
and this is my injection code in my fragment and activity
private val myViewModel by sharedViewModel<MainViewModel>()
but it gives me the following error
java.lang.NoSuchMethodError: No virtual method getRootScope()Lorg/koin/core/scope/ScopeDefinition; in class Lorg/koin/core/module/Module; or its super classes (declaration of 'org.koin.core.module.Module' appears in /data/app/com.example.koinapplication-0KW8HWC7rkVfXIFPydc82A==/base.apk)
at com.example.koinapplication.repo.RetrofitServiceKt$viewModelModule$1.invoke(RetrofitService.kt:64)
at com.example.koinapplication.repo.RetrofitServiceKt$viewModelModule$1.invoke(Unknown Source:2)
at org.koin.dsl.ModuleKt.module(Module.kt:31)
at org.koin.dsl.ModuleKt.module$default(Module.kt:29)
at com.example.koinapplication.repo.RetrofitServiceKt.<clinit>(RetrofitService.kt:28)
at com.example.koinapplication.repo.RetrofitServiceKt.getNetworkModule(RetrofitService.kt:17)
at com.example.koinapplication.KoinApplication$onCreate$1.invoke(KoinApplication.kt:15)
at com.example.koinapplication.KoinApplication$onCreate$1.invoke(KoinApplication.kt:9)
at org.koin.core.context.GlobalContext.startKoin$koin_core(GlobalContext.kt:68)
at org.koin.core.context.GlobalContextExtKt.startKoin(GlobalContextExt.kt:32)
at org.koin.core.context.GlobalContextExtKt.startKoin$default(GlobalContextExt.kt:31)
at com.example.koinapplication.KoinApplication.onCreate(KoinApplication.kt:12)
at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1182)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6460)
at android.app.ActivityThread.access$1300(ActivityThread.java:219)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1859)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.L..
The online guide says just declare module and inject but its not happening
how do i get around it?
Any help will be hugely appreciated
Related
I am getting this exception while unit testing the viewModel.
Exception in thread "UI thread #coroutine#1" java.lang.NullPointerException: Parameter specified as non-null is null: method androidx.paging.CachedPagingDataKt.cachedIn, parameter <this>
at androidx.paging.CachedPagingDataKt.cachedIn(CachedPagingData.kt)
at com.sarmad.newsprism.news.ui.NewsViewModel$getNewsStream$1.invokeSuspend(NewsViewModel.kt:46)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:829)
Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [CoroutineId(1), "coroutine#1":StandaloneCoroutine{Cancelling}#67526363, Dispatchers.Main]
expected:<false> but was:<true>
Expected :false
Actual :true
I want to test if newsViewModel.getNewsStream() is called, it should start loading, stop loading and expose updated UiState to NewsFragment so fragment can call adapter.submitData(data). But, there is an exception indication an error (I am a beginner and I can't understand that even after research for a good amount of time) in cachedIn(viewModelScope) operator while I am collecting flow in viewModel.
NewsViewModel
package com.sarmad.newsprism.news.ui
#HiltViewModel
class NewsViewModel #Inject constructor(
private val newsRepository: NewsRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
companion object {
const val KEY_SUBREDDIT = "us"
const val DEFAULT_SUBREDDIT = "androiddev"
}
init {
if (!savedStateHandle.contains(KEY_SUBREDDIT)) {
savedStateHandle[KEY_SUBREDDIT] = DEFAULT_SUBREDDIT
}
}
#OptIn(ExperimentalCoroutinesApi::class)
val articles = savedStateHandle.getLiveData<String>(KEY_SUBREDDIT)
.asFlow()
.flatMapLatest {
newsRepository.getBreakingNewsStream(it)
}.cachedIn(viewModelScope)
private val _userMessage = MutableStateFlow<String?>(null)
private val _isLoading = MutableStateFlow(false)
private val _newsArticles = articles
val uiState: StateFlow<NewsItemListUiState> = combine(
_isLoading, _userMessage, _newsArticles
) { isLoading, userMessage, newsArticles ->
NewsItemListUiState(
news = newsArticles,
isLoading = isLoading,
userMessage = userMessage
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000L),
initialValue = NewsItemListUiState(isLoading = true)
)
fun refresh() {
_isLoading.value = true
viewModelScope.launch {
newsRepository.refreshTasks()
_isLoading.value = false
}
}
}
NewsRepository
package com.sarmad.newsprism.data.repository
import androidx.paging.PagingData
import com.sarmad.newsprism.data.entities.NewsResponse
import kotlinx.coroutines.flow.Flow
import com.sarmad.newsprism.data.Result
import com.sarmad.newsprism.data.entities.Article
interface NewsRepository {
suspend fun getSearchedNewsStream(searchQuery: String, pageNumber: Int):
Flow<NewsResponse>
suspend fun getBreakingNewsStream(countryCode: String): Flow<PagingData<Article>>
}
NewsRepositoryImpl
package com.sarmad.newsprism.data.repository
import android.util.Log
import androidx.paging.*
import com.sarmad.newsprism.data.Result
import com.sarmad.newsprism.data.entities.Article
import com.sarmad.newsprism.data.entities.NewsResponse
import com.sarmad.newsprism.data.localdatasource.ArticleDao
import com.sarmad.newsprism.data.localdatasource.ArticleDatabase
import com.sarmad.newsprism.data.localdatasource.RemoteKeysDao
import com.sarmad.newsprism.data.paging.mediaters.NewsRemoteMediator
import com.sarmad.newsprism.data.remotedatasource.api.NewsApi
import com.sarmad.newsprism.utils.Constants.Companion.PAGING_CONFIG_PAGE_SIZE
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.mapLatest
import javax.inject.Inject
private const val TAG = "NewsRepositoryImpl"
class NewsRepositoryImpl #Inject constructor(
private val api: NewsApi,
private val articleDao: ArticleDao,
private val articleDatabase: ArticleDatabase,
private val remoteKeysDao: RemoteKeysDao,
) : NewsRepository {
override suspend fun getSearchedNewsStream(
searchQuery: String,
pageNumber: Int
): Flow<NewsResponse> = flow {
val searchedNewsResponse = api.searchNews(searchQuery, pageNumber)
if (searchedNewsResponse.isSuccessful) searchedNewsResponse.body()
?.let { newsList -> emit(newsList) }
else emptyFlow<NewsResponse>()
}
#OptIn(ExperimentalPagingApi::class)
override suspend fun getBreakingNewsStream(
countryCode: String
): Flow<PagingData<Article>> {
return Pager(
config = PagingConfig(
pageSize = PAGING_CONFIG_PAGE_SIZE
),
remoteMediator = NewsRemoteMediator(articleDatabase, articleDao, remoteKeysDao, api),
pagingSourceFactory = { articleDao.getNewsStream() }
).flow
}
}
NewsRemoteMadiator
package com.sarmad.newsprism.data.paging.mediaters
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import com.sarmad.newsprism.data.remotedatasource.api.NewsApi
import com.sarmad.newsprism.data.entities.Article
import com.sarmad.newsprism.data.entities.ArticleRemoteKey
import com.sarmad.newsprism.data.localdatasource.ArticleDao
import com.sarmad.newsprism.data.localdatasource.ArticleDatabase
import com.sarmad.newsprism.data.localdatasource.RemoteKeysDao
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.math.ceil
#OptIn(ExperimentalPagingApi::class)
class NewsRemoteMediator #Inject constructor(
private val articleDatabase: ArticleDatabase,
private val articleDao: ArticleDao,
private val remoteKeysDao: RemoteKeysDao,
private val api: NewsApi
) : RemoteMediator<Int, Article>() {
override suspend fun initialize(): InitializeAction {
val newsCacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS)
val isSkipRefresh = remoteKeysDao.getLastUpdateTime()?.let {
System.currentTimeMillis() - it >= newsCacheTimeout
}
return if (isSkipRefresh == true) {
InitializeAction.SKIP_INITIAL_REFRESH
} else {
InitializeAction.LAUNCH_INITIAL_REFRESH
}
}
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Article>
): MediatorResult {
return try {
val currentPage = when (loadType) {
LoadType.REFRESH -> {
val remoteKey = getRemoteKeyClosestToCurrentPosition(state)
remoteKey?.nextPage?.minus(1) ?: 1
}
LoadType.PREPEND -> {
val remoteKey = getRemoteKeyForFirstItem(state)
val prevPage = remoteKey?.prevPage ?: return MediatorResult.Success(
remoteKey != null
)
prevPage
}
LoadType.APPEND -> {
val remoteKey = getRemoteKeyForLastItem(state)
val nextPage =
remoteKey?.nextPage
?: return MediatorResult.Success(remoteKey != null)
nextPage
}
}
val response = api.getBreakingNews("us", currentPage)
val totalPages = response.body()?.totalResults?.toDouble()?.div(20)?.let { pages ->
ceil(pages)
}?.toInt()
val endOfPaginationReached = totalPages == currentPage
val nextPage = if (endOfPaginationReached) null else currentPage.plus(1)
val prevPage = if (currentPage == 1) null else currentPage.minus(1)
articleDatabase.withTransaction {
if (loadType == LoadType.REFRESH) {
articleDao.deleteAllArticles()
remoteKeysDao.deleteAllArticleRemoteKeys()
}
response.body()?.let { response ->
val keys = articleDao.insertAll(response.articles)
val mappedKeysToArticles = keys.map { key ->
ArticleRemoteKey(
id = key.toInt(),
nextPage = nextPage,
prevPage = prevPage,
modifiedAt = System.currentTimeMillis()
)
}
remoteKeysDao.insertArticleRemoteKeys(mappedKeysToArticles)
}
}
MediatorResult.Success(endOfPaginationReached)
} catch (ex: java.lang.Exception) {
return MediatorResult.Error(ex)
}
}
private suspend fun getRemoteKeyClosestToCurrentPosition(
state: PagingState<Int, Article>
): ArticleRemoteKey? {
return state.anchorPosition?.let { pos ->
state.closestItemToPosition(pos)?.id?.let { id ->
remoteKeysDao.getArticleRemoteKey(id)
}
}
}
private suspend fun getRemoteKeyForFirstItem(
state: PagingState<Int, Article>
): ArticleRemoteKey? {
return state.pages.firstOrNull {
it.data.isNotEmpty()
}?.data?.firstOrNull().let {
it?.let { it1 -> remoteKeysDao.getArticleRemoteKey(it1.id) }
}
}
private suspend fun getRemoteKeyForLastItem(
state: PagingState<Int, Article>
): ArticleRemoteKey? {
return state.pages.lastOrNull {
it.data.isNotEmpty()
}?.data?.lastOrNull().let {
it?.let { it1 -> remoteKeysDao.getArticleRemoteKey(it1.id) }
}
}
}
NewsViewModelTest
package com.sarmad.newsprism.news.ui
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.sarmad.newsprism.data.remotedatasource.api.NewsApi
import com.sarmad.newsprism.data.repository.NewsRepository
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
internal class NewsViewModelTest {
#Mock
lateinit var newsRepository: NewsRepository
#Mock
private lateinit var newsApi: NewsApi
private lateinit var newsViewModel: NewsViewModel
#OptIn(DelicateCoroutinesApi::class)
private val mainThreadSurrogate = newSingleThreadContext("UI thread")
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
#OptIn(ExperimentalCoroutinesApi::class)
#Before
fun setUp() {
MockitoAnnotations.openMocks(this)
Dispatchers.setMain(mainThreadSurrogate)
newsViewModel = NewsViewModel(newsRepository)
}
#OptIn(ExperimentalCoroutinesApi::class)
#After
fun tearDown() {
Dispatchers.resetMain() // reset the main dispatcher to the original Main dispatcher
mainThreadSurrogate.close()
}
#OptIn(ExperimentalCoroutinesApi::class)
#Test
fun test_getNewsFlow() = runTest {
newsViewModel.getNewsStream("us")
assertEquals(true, newsViewModel.newsFlow.value.isLoading)
advanceUntilIdle()
assertEquals(
false,
newsViewModel.newsFlow.value.isLoading
)
assertNotNull(
newsViewModel.newsFlow.value.news
)
}
}
The best way to check a call is using a Mock.
Create an interface for your NewsViewModel like INewsViewModel and inject it by using the constructor or the Setup. Depending on your Mock package it can be created like:
//Implementation using Moq
Mock<INewsViewModel> mock = new Mock<INewsViewModel>();
mock.Setup(m => m.getNewsStream());
// Your test
mock.VerifyAll();
Moq also allows to create a Mock when the mocked class has an empty constructor.
The error that appears is as follows "Type mismatch: inferred type is String? but String is expected". How can I solve this problem?
The source code is as follows:
package com.example.submission2.Activity
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.example.submission2.Adapter.AdapterSectionPager
import com.example.submission2.ViewModel.DetailVM
import com.example.submission2.databinding.ActivityDetailBinding
class DetailActivity : AppCompatActivity() {
companion object{
const val EXTRA_USERNAME = "extra_username"
}
private lateinit var binding: ActivityDetailBinding
private lateinit var viewModel: DetailVM
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
}
val username = intent.getStringExtra(EXTRA_USERNAME)
viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(DetailVM::class.java)
viewModel.setPenggunaDetail(username)
viewModel.getPenggunaDetail().observe(this) {
if (it != null) {
binding.apply {
tvNamaDetail.text = it.name
tvUsernameDetail.text = it.login
tvCompanyDetail.text = it.company
tvEmailDetail.text = it.email
tvFollowersDetail.text = "${it.followers} Followers"
tvFollowingDetail.text = "${it.following} Follwing"
Glide.with(this#DetailActivity)
.load(it.avatar_url)
.transition(DrawableTransitionOptions.withCrossFade())
.centerCrop()
.into(ivDetailProfil)
}
}
}
val sectionPagerAdpter = AdapterSectionPager(this,supportFragmentManager)
binding.apply {
viewPager.adapter = sectionPagerAdpter
tabs.setupWithViewPager(viewPager)
}
}
}
error appears on the line "viewModel.set User Data(username)" username is used in extra_username which will be called in main
for main activity as follows:
package com.example.submission2.Activity
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.KeyEvent
import android.view.View
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.submission2.Adapter.AdapterPengguna
import com.example.submission2.DataBase.Pengguna
import com.example.submission2.R
import com.example.submission2.ViewModel.MainVM
import com.example.submission2.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: MainVM
private lateinit var adapter: AdapterPengguna
private fun searchPengguna(){
binding.apply {
val query = etSearch.text.toString()
if (query.isEmpty())return
showLoading(true)
viewModel.setSearchPengguna(query)
}
}
private fun showLoading(state: Boolean){
if (state){
binding.progressBarMain.visibility = View.VISIBLE
}else{
binding.progressBarMain.visibility = View.GONE
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
adapter = AdapterPengguna()
adapter.notifyDataSetChanged()
adapter.setOnItemClickCallback(object :AdapterPengguna.OnItemClickCallback{
override fun onItemCliked(data: Pengguna) {
Intent(this#MainActivity,DetailActivity::class.java).also {
it.putExtra(DetailActivity.EXTRA_USERNAME, data.login)
startActivity(it)
}
}
})
viewModel = ViewModelProvider(this,ViewModelProvider.NewInstanceFactory()).get(MainVM::class.java)
binding.apply {
rvPengguna.layoutManager = LinearLayoutManager(this#MainActivity)
rvPengguna.setHasFixedSize(true)
rvPengguna.adapter = adapter
btnSearch.setOnClickListener {
searchPengguna()
}
etSearch.setOnKeyListener { v, keyCode, event ->
if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER){
searchPengguna()
return#setOnKeyListener true
}
return#setOnKeyListener false
}
}
viewModel.getSearchPengguna().observe(this,{
if (it!= null){
adapter.setList(it)
showLoading(false
)
}
})
}
}
In your code there is no such line as viewModel.setUserData
I guess that the error occurs in the line viewModel.setPenggunaDetail(username)
In this case, you should pay attention to the fact that the all intent.getExtra calls returns nullable values.
Thus, if the setPenggunaDetail call expects a non-nullable argument, you must first check username value for null
I have implemented Result.Success and Result.Error in my ViewModel class but I am getting following error in my SecondActivity.kt C:\Users\Edgar\AndroidStudioProjects\GiphyAndroidApp\app\src\main\java\com\example\giphyandroidapp\ui\SecondActivity.kt: (52, 52): Type mismatch: inferred type is Result<List?> but List was expected
below my SecondActivity.kt
package com.example.giphyandroidapp.ui
import com.example.giphyandroidapp.utils.Result
import com.example.giphyandroidapp.utils.Error
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import com.example.giphyandroidapp.utils.Constants
import com.example.giphyandroidapp.viewmodel.GiphyTaskViewModel
import com.example.giphyandroidapp.adapters.GiphyTaskAdapter
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.GridLayoutManager
import com.example.giphyandroidapp.databinding.ActivitySecondBinding
import com.example.giphyandroidapp.utils.Success
import dagger.hilt.android.AndroidEntryPoint
#AndroidEntryPoint
class SecondActivity : AppCompatActivity() ,View.OnClickListener {
lateinit var binding: ActivitySecondBinding
val viewModel: GiphyTaskViewModel by viewModels()
var text: String = ""
lateinit var myadapter: GiphyTaskAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySecondBinding.inflate(layoutInflater)
setContentView(binding.root)
val intent = intent
if (intent.hasExtra("text")) {
// list= intent.getParcelableArrayListExtra<Parcelable>("response")!! as List<DataItem>
text = intent.getStringExtra("text").toString()
if (text != null) {
bindData()
}
}
binding.getresultbtn.setOnClickListener(this)
}
private fun bindData() {
viewModel.getGifsFromText(Constants.Api_Key, text, Constants.Limit)
viewModel.giphyresponse.observe(this,
{ response ->
when (response) {
is Success -> {
myadapter = GiphyTaskAdapter(this, response.data)
}
is Error -> {
} // Handle error case
}
binding.imgsrecycler.apply {
adapter = myadapter
layoutManager = GridLayoutManager(this#SecondActivity, 2)
visibility = View.VISIBLE
}
binding.progressBar.visibility = View.GONE
})
}
override fun onClick(v: View?) {
var mytext: String = ""
mytext = binding.txtword.text.toString()
if (mytext.equals("")) {
Toast.makeText(this, "you must enter word", Toast.LENGTH_LONG).show()
} else {
if (text.equals(mytext)) {
val builder = android.app.AlertDialog.Builder(this)
builder.setTitle("Winner")
builder.setMessage("Excellent , you win \n Play game again ?!")
builder.setPositiveButton("Yes") { dialog, which ->
startActivity(Intent(this, MainActivity::class.java))
}
builder.setNegativeButton("No") { dialog, which ->
dialog.dismiss()
}
builder.create().show()
} else {
val builder = android.app.AlertDialog.Builder(this)
builder.setTitle("Loser")
builder.setMessage("fail , you lose \n Game over ! \n Play game again ?!")
builder.setPositiveButton("Yes") { dialog, which ->
startActivity(Intent(this, MainActivity::class.java))
}
builder.setNegativeButton("No") { dialog, which ->
dialog.dismiss()
}
builder.create().show()
}
}
}
}
below GiphyAdapter.kt
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import com.example.giphyandroidapp.databinding.ListItemBinding
import com.example.giphyandroidapp.model.gifsresponse.DataItem
import com.example.giphyandroidapp.model.gifsresponse.Images
import com.example.giphyandroidapp.utils.Result
class GiphyTaskAdapter(val context: Context,val list:List<DataItem>) : RecyclerView.Adapter<GiphyTaskAdapter.GiphyTaskViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GiphyTaskAdapter.GiphyTaskViewHolder {
return GiphyTaskViewHolder(ListItemBinding.inflate(LayoutInflater.from(parent.context),parent,false))
}
override fun onBindViewHolder(holder: GiphyTaskAdapter.GiphyTaskViewHolder, position: Int) {
var item:DataItem = list.get(position)
var imageitem: Images =item.images
holder.binding.apply {
img.load(imageitem.original.url){
crossfade(true)
crossfade(1000)
}
}
holder.itemView.setOnClickListener {mview->
}
}
override fun getItemCount()=list.size
inner class GiphyTaskViewHolder(val binding: ListItemBinding):
RecyclerView.ViewHolder(binding.root)
}
below my ViewModel class where I have implemented Result.Success and Result.Error logic
#HiltViewModel
class GiphyTaskViewModel
#Inject
constructor(private val giphyTaskRepository: GiphyTaskRepository):ViewModel()
{
var giphyresponse = MutableLiveData<Result<List<DataItem>?>>()
fun getGifsFromText(apikey:String,text:String,limit:Int)= viewModelScope.launch {
giphyTaskRepository.getGifsFromText(apikey,text,limit).let { response->
if(response?.isSuccessful){
var list=response.body()?.data
giphyresponse.postValue(Success(list))
}else{
Error(Exception(response.message()))
}
}
}
}
below Repository class
class GiphyTaskRepository
#Inject
constructor(private val giphyTaskApiService: GiphyTaskApiService)
{
suspend fun getGifsFromText(apikey:String,text:String,limit:Int)=
giphyTaskApiService.getGifsFromText(apikey,text,limit)
}
below my network interface
interface GiphyTaskApiService {
#GET("gifs/search")
suspend fun getGifsFromText(
#Query("api_key") api_key:String,
#Query("q") q:String ,
#Query("limit") limit:Int
):Response<GiphyResponse>
}
below GiphyResponse.kt
#Parcelize
data class GiphyResponse(
#field:SerializedName("pagination")
val pagination: Pagination,
#field:SerializedName("data")
val data: List<DataItem>,
#field:SerializedName("meta")
val meta: Meta
) : Parcelable
below Result class
sealed class Result<T>
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Any?>()
I want to know what I have to do in order to successfully implement result.success and result.error class and pass correct paremeter so that avoid mismatch error
I am getting following mismatch after implementing murat's answer
GiphyTaskAdapter expects List, but in ViewModel you have Result<List?>.
class GiphyTaskAdapter(val context: Context,val list:List<DataItem>)
var giphyresponse = MutableLiveData<Result<List<DataItem>?>>()
So, for example, you can change a type in ViewModel.
var giphyresponse: MutableLiveData<List<DataItem>> = MutableLiveData()
...
val list: List<DataItem> = response.body()?.data ?: emptyList()
giphyresponse.postValue(list)
UPD:
sealed class can be handled as Marat wrote. Also, you should handle null type.
when(response) {
is Success -> {
val list: List<DataItem> = response.data ?: emptyList()
myadapter = GiphyTaskAdapter(this, list)
.......
}
is Error -> // Handle error case
}
Updated
You need to check your response if it is successful or not. Something like:
when(response) {
is Success -> {
myadapter = GiphyTaskAdapter(this, response.data)
.......
}
is Error -> // Handle error case
}
I want to unit test a method in viewmodal but everytime i failed, and had gone through many of the websites and stack answers but none of them helped out.
I just wanted to test a method in my viewmodal that is loadWeatherForeCast
I had gone through the following links,
Error calling Dispatchers.setMain() in unit test
https://android.jlelse.eu/mastering-coroutines-android-unit-tests-8bc0d082bf15
https://proandroiddev.com/mocking-coroutines-7024073a8c09
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.junit5.MockKExtension
import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import go_jek.domain.entities.LocationTemperature
import go_jek.domain.interactor.Result
import go_jek.domain.interactor.weatherUseCase.WeatherParam
import go_jek.domain.interactor.weatherUseCase.WeatherUseCase
import go_jek.domain.repository.ApiDataSource
import go_jek.utility.dateUtils.DateUtils
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
class WeatherViewModelTest {
#get:Rule
val rule = InstantTaskExecutorRule()
#MockK
lateinit var apiDataSource: ApiDataSource //Interface
#MockK
lateinit var dateUtilImpl: DateUtils //Interface
#MockK
lateinit var weatherParam: WeatherParam
#MockK
lateinit var locationTemperatureOutput: LocationTemperature
private lateinit var weatherUseCase: WeatherUseCase
private lateinit var weatherViewModel: WeatherViewModel
#BeforeAll
fun setup() {
MockKAnnotations.init(this)
weatherUseCase = WeatherUseCase(apiDataSource)
weatherViewModel = WeatherViewModel(weatherUseCase, dateUtilImpl, Dispatchers.Unconfined)
}
#Test
fun check() = runBlocking {
every {
weatherUseCase.execute(any(), any(), any())
} answers {
thirdArg<(Result<LocationTemperature>) -> Unit>().invoke(Result.Success(locationTemperatureOutput))
}
weatherViewModel.loadWeatherForeCast(32.45, 72.452)
verify(atLeast = 1) {
apiDataSource.getWeatherInfo(weatherParam)
}
}
}
interface ApiDataSource {
fun getWeatherInfo(weatherParam: WeatherParam): Result<LocationTemperature>
}
class WeatherUseCase(var apiDataSource: ApiDataSource) : UseCase<LocationTemperature, WeatherParam>() {
override suspend fun run(params: WeatherParam): Result<LocationTemperature> = apiDataSource.getWeatherInfo(params)
}
class WeatherParam(
val apiKey: String = BuildConfig.appixu_secretkey,
val location: String
) : UseCase.NoParams()
class LocationTemperature(val location: Location, val current: Current, val forecast: Forecast) : Parcelable
class WeatherViewModel (val weatherUseCase: WeatherUseCase,
val dateUtilImpl: DateUtils,
val uiContext: CoroutineContext = Dispatchers.Main) : ViewModel(), CoroutineScope {
private val job: Job
private val _locationLiveData = MutableLiveData<LocationTemperature>()
val locationLiveData: LiveData<LocationTemperature>
private val _error: MutableLiveData<String> = MutableLiveData()
var error: LiveData<String> = _error
private val loadingState = MutableLiveData<Boolean>()
val loadingLiveData = loadingState
override val coroutineContext: CoroutineContext
get() = uiContext + job
init {
job = Job()
locationLiveData = Transformations.map(_locationLiveData) {
it.apply {
forecast.forecastday.forEach {
it.dayOfWeek = dateUtilImpl.format(it.date, DateUtils.FOR_YYYY_H_MM_H_DD, FULL_DAY)
}
}
}
}
fun handleError(error: Exception) {
loadingState.value = false
_error.value = error.localizedMessage
}
fun loadWeatherForeCast(latitude: Double, longitude: Double) {
val weatherParam = WeatherParam(location = String.format(Locale.getDefault(), "%1f, %2f",
latitude, longitude))
weatherUseCase.execute(this, weatherParam)
{
when (it) {
is Result.Success -> {
loadingState.value = false
_locationLiveData.value = it.data
}
is Result.Error -> {
handleError(it.exception)
}
}
}
}
override fun onCleared() {
job.cancel()
super.onCleared()
}
}
To use mockK with coroutines you need to use the new "coEvery" and "coVerify" functions and it will work better :
https://mockk.io/#coroutines
I have Dagger2 injected property in my presenter.
#Inject lateinit var dataInteractor: DataInteractor
It is accessed in couple of methods. In one of them loadAppointments() everything works fine but in another refund() UninitializedPropertyAccessException is thrown. The code has been working well for a while and this issue has raised the only couple of days ago.
No Kotlin updates were installed before.
import android.util.Log
import co.example.Application
import co.example.domain.model.entity.AppointmentsEntity
import co.example.domain.model.entity.ProvidersEntity
import co.example.interactor.data.DataInteractor
import co.example.domain.di.base.RxDisposablePresenter
import co.example.view.operation.OperationView
import io.reactivex.android.schedulers.AndroidSchedulers
import java.util.*
import javax.inject.Inject
class AppointmentsPresenter : RxDisposablePresenter<AppointmentsPresenter.View>() {
#Inject
lateinit var dataInteractor: DataInteractor
private lateinit var operationView: OperationView<*>
private val results: ArrayList<AppointmentsEntity.AppointmentEntity> = ArrayList()
private val resultsHistory: ArrayList<AppointmentsEntity.AppointmentEntity> = ArrayList()
override fun onTakeView(view: View?) {
super.onTakeView(view)
operationView = view?.operationView()!!
}
fun loadAppointments() {
val userID = Application.appComponent.userInternalStorage().userID()
if (userID != null) {
add(dataInteractor
.getAppointments(userID, LIMIT, OFFSET)
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe { operationView.showProgress() }
.doFinally {
operationView.hideProgress()
view?.onAppointmentsDataFilled(results, resultsHistory)
}
.subscribe(
{
divideUpcomingAndCompleteAppointments(it)
view?.unblockAppointments()
},
{
val message = it.message
if (message!!.contains("blocked")) {
view?.blockAppointments()
operationView.showError("Error")
return#subscribe
}
operationView.showError(message)
}
))
}
}
fun refund(position: Int?) {
add(dataInteractor
.refund(results[position!!].id!!)
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe { operationView.showProgress() }
.doFinally {
operationView.hideProgress()
}
.subscribe(
{
view?.onRefundCompleted()
},
{
val message = it.message
operationView.showError(message!!)
}
))
}
private fun divideUpcomingAndCompleteAppointments(appointmentsEntity: AppointmentsEntity) {
results.clear()
resultsHistory.clear()
for (appointment in appointmentsEntity.results!!) {
if (appointment.status.equals(STATUS_COMPLETE, true)) {
resultsHistory.add(appointment)
} else {
results.add(appointment)
}
}
}
fun loadProviderById(doctorId: Long?, onDoctorRetrivedListener: (ProvidersEntity.ProviderEntity) -> Unit) {
add(
dataInteractor
.getProviders(0, 100, 0, 0)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ providersEntity ->
val doctor = providersEntity
.results
?.filter {
Log.d("ATAT", "loadProviderById: ${it.user_id}")
it.user_id == doctorId
}
doctor?.let {
if (it.isNotEmpty()) {
onDoctorRetrivedListener.invoke(doctor[0])
}
}
},
{
val message = it.message
operationView.showError(message!!)
}))
}
interface View {
fun operationView(): OperationView<*>
fun onAppointmentsDataFilled(appointments: ArrayList<AppointmentsEntity.AppointmentEntity>?,
appointmentsHistory: ArrayList<AppointmentsEntity.AppointmentEntity>?)
fun onRefundCompleted()
fun blockAppointments()
fun unblockAppointments()
}
companion object {
const val STATUS_COMPLETE = "Complete"
const val LIMIT: Long = 1000
const val OFFSET: Long = 0
}
}
Here is the stack trace:
2018-12-13 11:59:29.359 4808-4808/co.example E/AndroidRuntime: FATAL EXCEPTION: main
Process: co.example, PID: 4808
kotlin.UninitializedPropertyAccessException: lateinit property dataInteractor has not been initialized
at co.example.presenter.appointements.AppointmentsPresenter.refund(AppointmentsPresenter.kt:59)
at co.example.view.appointements.AppointmentsFragment.refundRequest(AppointmentsFragment.kt:123)
at co.example.activity.MainActivity.onRefundRequested(MainActivity.kt:295)
at co.example.view.dialog.RefundDialogFragment$onCreateDialog$1.onClick(RefundDialogFragment.kt:16)
at com.android.internal.app.AlertController$ButtonHandler.handleMessage(AlertController.java:172)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
UPD:
Doesn't reproduce if fragment that calls these methods is created as singleton.