I am getting an error in my project. What is the problem? can you help me?
android.util.AndroidRuntimeException: Animators may only be run on Looper threads
at com.nisaefendioglu.movieapp.ui.DetailMovieActivity$addToFav$1.invokeSuspend(DetailMovieActivity.kt:65)
MyCode :
DetailMovieActivity:
class DetailMovieActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailMovieBinding
var b:Bundle?=null
private lateinit var appDb : MovieDatabase
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding= ActivityDetailMovieBinding.inflate(layoutInflater);
appDb= MovieDatabase.getDatabase(this);
setContentView(binding.root)
b=intent.extras
val i=b?.getString("imdbid")
val apikey="93b3e8f8"
ApiClient.instances.getDetailMovie(i,apikey).enqueue(object :Callback<MovieDetailData> {
override fun onResponse(
call: Call<MovieDetailData>,
response: Response<MovieDetailData>
) {
binding.tvType.text = response.body()?.Release
binding.tvPlot.text=response.body()?.plot
Glide.with(this#DetailMovieActivity).load(response.body()?.poster)
.into(binding.imgPoster)
binding.imgToolbarBtnFav.setOnClickListener(){
addToFav(response.body());
}
}
override fun onFailure(call: Call<MovieDetailData>, t: Throwable) {
TODO("Not yet implemented")
}
})
binding.imgToolbarBtnBack.setOnClickListener {
finish()
}
}
private fun addToFav(body: MovieDetailData?) {
GlobalScope.launch(Dispatchers.IO) {
if (body?.let { appDb.movieDao().getById(it.Title)} !=null ) {
binding.imgToolbarBtnFav.setBackgroundResource(R.drawable.favorite_bg);
return#launch;
}else{
binding.imgToolbarBtnFav.setBackgroundResource(R.drawable.favorite_bg);
body?.let { appDb.movieDao().insert(it) }
}
}
}
}
MovieDatabase:
#Database(entities = [MovieDetailData::class],version = 2, exportSchema = false)
abstract class MovieDatabase: RoomDatabase() {
abstract fun movieDao() : MovieDao
companion object{
#Volatile
private var INSTANCE : MovieDatabase? = null
fun getDatabase(context: Context): MovieDatabase {
val tempInstance = INSTANCE
if(tempInstance != null){
return tempInstance
}
synchronized(this){
val instance = Room.databaseBuilder(
context.applicationContext,
MovieDatabase::class.java,
"movies2"
).build()
INSTANCE = instance
return instance
}
}
}
}
Hello, I am getting an error in my project. What is the problem? can you help me?
Hello, I am getting an error in my project. What is the problem? can you help me?
Hello, I am getting an error in my project. What is the problem? can you help me?
This is the problem: .launch(Dispatchers.IO)
Dispatchers.IO is a thread pool that is completely independent from Android's Looper system that various APIs like Glide use to run callbacks in asynchronous functions. Also, many Android View-related classes must be called on the Android main thread (which also has a Looper).
When in an Activity, you should use lifecycleScope to launch your coroutines, and you should not change the dispatcher since it appropriately uses Dispatchers.Main by default.
private fun addToFav(body: MovieDetailData?) {
lifecycleScope.launch {
if (body?.let { appDb.movieDao().getById(it.Title)} != null) {
binding.imgToolbarBtnFav.setBackgroundResource(R.drawable.favorite_bg) //TODO?
}else{
binding.imgToolbarBtnFav.setBackgroundResource(R.drawable.favorite_bg)
body?.let { appDb.movieDao().insert(it) }
}
}
}
You should only use Dispatchers.IO when you are calling blocking functions.
Suggestion: I don't think you should make body nullable in this function since it cannot do anything useful with a null body. The null checks make the code more confusing. You should push the null check to the caller. Then this function can be simplified.
you cannot use background thread to work with UI.
here is solution
private fun addToFav(body: MovieDetailData?) {
lifecycleScope.launch {
if (body?.let { appDb.movieDao().getById(it.Title)} !=null ) {
binding.imgToolbarBtnFav.setBackgroundResource(R.drawable.favorite_bg);
return#launch;
}else{
binding.imgToolbarBtnFav.setBackgroundResource(R.drawable.favorite_bg);
body?.let { appDb.movieDao().insert(it) }
}
}
}
Related
I want to create a singleton with Coroutine to load image from network. I have done implement the singleton and can load network image into imageView. Here is my singleton class.
class Singleton(context: Context) {
private val TAG = "Singleton"
private val scope =
CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineExceptionHandler { _, exception ->
Log.e(TAG, "Caught $exception")
})
private var job:Job? = null
companion object {
private var INSTANCE: Singleton? = null
#Synchronized
fun with(context: Context): Singleton {
require(context != null) {
"ImageLoader:with - Context should not be null."
}
return INSTANCE ?: Singleton(context).also {
INSTANCE = it
Log.d("ImageLoader", "First Init")
}
}
}
private fun onAttachStateChange(imageView: ImageView, job: Job) {
imageView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
}
override fun onViewDetachedFromWindow(v: View?) {
job.cancel()
}
})
}
fun loadImage(url: String, imageView: ImageView) {
job = scope.launch {
try {
updateData(URL(url), imageView)
} catch (e: CancellationException) {
Log.d(TAG, "work cancelled!")
}
}.also {
onAttachStateChange(imageView, it)
}
}
suspend fun updateData(url: URL, imageView: ImageView) = run {
fetchImage(url)?.apply { imageView.setImageBitmap(this) }
?: imageView.setImageResource(R.drawable.ic_launcher_background)
}
fun stopUpdate() {
scope.cancel()
}
private suspend fun fetchImage(url: URL): Bitmap? {
return withContext(Dispatchers.IO) {
try {
val connection = url.openConnection() as HttpURLConnection
val bufferedInputStream = BufferedInputStream(connection.inputStream)
BitmapFactory.decodeStream(bufferedInputStream)
} catch (e: Exception) {
Log.e("TAG", e.toString())
null
}
}
}
}
My problem is when I cancel my coroutine scope in onDestroy() at ActivityB and than use my singleton again in ActivityA it won't do anything cause the scope have been cancel(). So is there any way to use Coroutine in singleton properly with scope.cancel() when activity is onDestroy(). Here is a demo:
class MainActivityA : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_activity)
Singleton.with(this).updateData(url, imageView)
}
}
class MainActivityB : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_activity)
}
override fun onDestroy() {
super.onDestroy()
// Do not need to call scope.cancel(). Cause when the view is
// detached it will cancel the job.
// Singleton.with(this).stopUpdate()
}
}
Edited
I have come up with an idea and have added into Singleton class. Using view.onAttachStateChange to detect whether the view is still attached to the window. If is detached then we can cancel the job. Is this a good way to doing so?
Singleton by definition lives forever, so I'm not really sure it makes sense to cancel its scope. What if you would need to use your singleton from multiple components at the same time? They would cancel jobs of other components.
To make sure you don't leak jobs of destroyed components, you can either create a child job per component and put all tasks under it or just do not define a custom scope at all and reuse the coroutine context of the caller.
I'm working on Android for a while but it's the first time I have to write some unit tests.
I have a design pattern in MVP so basically I have my Presenter, which have a contract (view) and it's full in kotlin, using coroutines.
Here is my Presenter class : The Repository and SomeOtherRepository are kotlin object so it's calling methods directly (The idea is to not change the way it's working actually)
class Presenter(private val contractView: ContractView) : CoroutinePresenter() {
fun someMethod(param1: Obj1, param2: Obj2) {
launch {
try {
withContext(Dispatchers.IO) {
val data = SomeService.getData() ?: run { throw Exception(ERROR) } // getData() is a suspend function
Repository.doRequest(param1, param2) // doRequest() is a suspend function also
}.let { data ->
if (data == null) {
contractView.onError(ERROR)
} else {
if (SomeOtherRepository.validate(data)) {
contractView.onSuccess()
} else {
contractView.onError(ERROR)
}
}
} catch (exception: Exception) {
contractView.onError(exception)
}
}
}
}
So the goal for me is to create unit test for this Presenter class so I created the following class in order to test the Presenter. Here is the Test implementation :
I read a lot of articles and stackoverflow links but still have a problem.
I setup a TestCoroutineRule which is like this :
#ExperimentalCoroutinesApi
class TestCoroutineRule(
private val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher(), TestCoroutineScope by TestCoroutineScope() {
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
private fun TestCoroutineRule.runBlockingTest(block: suspend () -> Unit) =
testDispatcher.runBlockingTest { block() }
}
And here is the PresenterTest implementation :
#ExperimentalCoroutinesApi
class PresenterTest {
#get:Rule
val testCoroutineRule = TestCoroutineRule()
#Mock
private lateinit var view: ContractView
#Mock
private lateinit var repository: Repository
private lateinit var presenter: Presenter
#Before
fun setUp() {
MockitoAnnotations.initMocks(this)
presenter = Presenter(view)
}
#Test
fun `test success`() =
testCoroutineRule.runBlockingTest {
// Given
val data = DummyData("test", 0L)
// When
Mockito.`when`(repository.doRequest(param1, param2)).thenReturn(data)
// Then
presenter.someMethod("test", "test")
// Assert / Verify
Mockito.verify(view, Mockito.times(1)).onSuccess()
}
}
The problem I have is the following error Wanted but not invoked: view.onSuccess(); Actually there were zero interactions with this mock.
The ContractView is implemented in the Activity so I was wondering if I have to use Robolectric in order to trigger the onSuccess() method within the Activity context. I also think that I have a problem regarding the usage of coroutines maybe. I tried a lot of things but I always got this error on the onSuccess et onError view, if anyone could help, would be really appreciated :)
There could be other problems, but at a minimum you are missing:
Mockito.`when`(someOtherRepository.validate(data)).thenReturn(data)
Mockito.`when`(someService.getData()).thenReturn(data)
Use your debugger and check your logs to inspect what the test is doing
I have a Repository defined as the following.
class StoryRepository {
private val firestore = Firebase.firestore
suspend fun fetchStories(): QuerySnapshot? {
return try {
firestore
.collection("stories")
.get()
.await()
} catch(e: Exception) {
Log.e("StoryRepository", "Error in fetching Firestore stories: $e")
null
}
}
}
I also have a ViewModel like this.
class HomeViewModel(
application: Application
) : AndroidViewModel(application) {
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private val storyRepository = StoryRepository()
private var _stories = MutableLiveData<List<Story>>()
val stories: LiveData<List<Story>>
get() = _stories
init {
uiScope.launch {
getStories()
}
uiScope.launch {
getMetadata()
}
}
private suspend fun getStories() {
withContext(Dispatchers.IO) {
val snapshots = storyRepository.fetchStories()
// Is this correct?
if (snapshots == null) {
cancel(CancellationException("Task is null; local DB not refreshed"))
return#withContext
}
val networkStories = snapshots.toObjects(NetworkStory::class.java)
val stories = NetworkStoryContainer(networkStories).asDomainModel()
_stories.postValue(stories)
}
}
suspend fun getMetadata() {
// Does some other fetching
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
As you can see, sometimes, StoryRepository().fetchStories() may fail and return null. If the return value is null, I would like to not continue what follows after the checking for snapshots being null block. Therefore, I would like to cancel that particular coroutine (the one that runs getStories() without cancelling the other coroutine (the one that runs getMetadata()). How do I achieve this and is return-ing from withContext a bad-practice?
Although your approach is right, you can always make some improvements to make it simpler or more idiomatic (especially when you're not pleased with your own code).
These are just some suggestions that you may want to take into account:
You can make use of Kotlin Scope Functions, or more specifically the let function like this:
private suspend fun getStories() = withContext(Dispatchers.IO) {
storyRepository.fetchStories()?.let { snapshots ->
val networkStories = snapshots.toObjects(NetworkStory::class.java)
NetworkStoryContainer(networkStories).asDomainModel()
} ?: throw CancellationException("Task is null; local DB not refreshed")
}
This way you'll be returning your data or throwing a CancellationException if null.
When you're working with coroutines inside a ViewModel you have a CoroutineScope ready to be used if you add this dependendy to your gradle file:
androidx.lifecycle:lifecycle-viewmodel-ktx:{version}
So you can use viewModelScope to build your coroutines, which will run on the main thread:
init {
viewModelScope.launch {
_stories.value = getStories()
}
viewModelScope.launch {
getMetadata()
}
}
You can forget about cancelling its Job during onCleared since viewModelScope is lifecycle-aware.
Now all you have left to do is handling the exception with a try-catch block or with the invokeOnCompletion function applied on the Job returned by the launch builder.
How to Unit Test MVVM with Koin ?
i've try to testing : link
But, i don't know why i get error("No Data in ViewModel") in ViewModelTest fun getLookUpLeagueList()
Repository
class LookUpLeagueRepository {
fun getLookUpLeague(idLeague: String): MutableLiveData<LookUpLeague> {
val lookUpLeague = MutableLiveData<LookUpLeague>()
APIService().getLookUpLeague(idLeague).enqueue(object : Callback<LookUpLeague> {
override fun onFailure(call: Call<LookUpLeague>, t: Throwable) {
d("TAG", "lookUpLeagueOnFailure ${t.localizedMessage}")
}
override fun onResponse(call: Call<LookUpLeague>, response: Response<LookUpLeague>) {
lookUpLeague.value = response.body()
}
})
return lookUpLeague
}
}
ViewModel
class LookUpLeagueViewModel(private val lookUpLeagueRepository: LookUpLeagueRepository) :
ViewModel() {
var lookUpLeagueList = MutableLiveData<LookUpLeague>()
fun getLookUpLeagueList(idLeague: String) {
lookUpLeagueList = lookUpLeagueRepository.getLookUpLeague(idLeague)
}
}
Module
val lookUpLeagueModule = module {
single { LookUpLeagueRepository() }
viewModel { LookUpLeagueViewModel(get()) }
}
ViewModel Test
class LookUpLeagueViewModelTest : KoinTest {
val lookUpLeagueViewModel: LookUpLeagueViewModel by inject()
val idLeague = "4328"
#get:Rule
val rule = InstantTaskExecutorRule()
#Mock
lateinit var observerData: Observer<LookUpLeague>
#Before
fun before() {
MockitoAnnotations.initMocks(this)
startKoin {
modules(lookUpLeagueModule)
}
}
#After
fun after() {
stopKoin()
}
#Test
fun getLookUpLeagueList() {
lookUpLeagueViewModel.lookUpLeagueList.observeForever(observerData)
lookUpLeagueViewModel.getLookUpLeagueList(idLeague)
val value = lookUpLeagueViewModel.lookUpLeagueList.value ?: error("No Data in ViewModel")
Mockito.verify(observerData).onChanged(value)
}
}
#Test
fun getLookUpLeagueList() {
lookUpLeagueViewModel.lookUpLeagueList.observeForever(observerData)
...
}
At this time lookUpLeagueList is an instance of MutableLiveData. Say this is MutableLiveData #1.
lookUpLeagueViewModel.getLookUpLeagueList(idLeague)
Executing the line above would call LookUpLeagueViewModel.getLookUpLeagueList function. Let's take a look inside it.
lookUpLeagueList = lookUpLeagueRepository.getLookUpLeague(idLeague)
A totally new MutableLiveData is created inside LookUpLeagueRepository. That is not the same one as the one observerData is observing. At this time lookUpLeagueViewModel.lookUpLeagueList refers to the new one, MutableLiveData #2 because you re-assigned it to var lookUpLeagueList.
val value = lookUpLeagueViewModel.lookUpLeagueList.value ?: error("No Data in ViewModel")
Therefore, you're actually querying against MutableLiveData #2 which is new, not observed, and empty. That's why value is null. Instead of declaring as var, you should make it val. Don't re-assign the variable, setValue or postValue to propagate the change.
I've a WeatherRepository class which calls the WeatherProvider class to start fetching the weather.
After the weather is successfully fetched, I simply post that weather using postValue function but the observer on that livedata in the WeatherRepository class's init block never gets called.
I am confused as what am I missing...
Any insights would be extremely helpful.
Here's my code for Repository and Provider:
class WeatherRepository #Inject constructor(private var weatherDao: WeatherDao, private var weatherProvider: WeatherProvider) {
private fun startFetchWeatherService() {
weatherProvider.startFetchWeatherService()
}
init {
// Control flow always gets to this point
var weather = weatherProvider.getDownloadedWeather()
weather.observeForever { // This observer never gets called
if (it != null) AsyncTask.execute { insertWeather(it) }
}
if (isFetchNeeded()) {
startFetchWeatherService() // Android Studio always execute this line since no data is inserted by observer and fetch is needed
}
}
....
}
class WeatherProvider(private val context: Context) {
private val mDownloadedWeather = MutableLiveData<List<Weather>>()
...
fun getDownloadedWeather(): MutableLiveData<List<Weather>> = mDownloadedWeather
fun getFromInternet() {
...
call.enqueue(object : Callback<WorldWeatherOnline> {
override fun onFailure(call: Call<WorldWeatherOnline>?, t: Throwable?) {} // TODO show error
override fun onResponse(call: Call<WorldWeatherOnline>?, response: Response<WorldWeatherOnline>?) {
if (response != null) {
val weather = response.body()?.data
if (weather != null) {
mDownloadedWeather.postValue(WeatherUtils.extractValues(weather)) // app always gets to this point and WeatherUtils successfully returns the List of weathers full of data
}
}
}
})
}
fun startFetchWeatherService() {
val intentToFetch = Intent(context, WeatherSyncIntentService::class.java)
context.startService(intentToFetch)
}
}
...
// Dependency injection always works
// Here's my dagger2 module (other modules are very simillar to this one)
#Module
class ApplicationModule(private val weatherApplication: WeatherApplication) {
#Provides
internal fun provideWeatherApplication(): WeatherApplication {
return weatherApplication
}
#Provides
internal fun provideApplication(): Application {
return weatherApplication
}
#Provides
#Singleton
internal fun provideWeatherProvider(context: WeatherApplication): WeatherProvider {
return WeatherProvider(context)
}
}
#Singleton
class CustomViewModelFactory constructor(private val weatherRepository: WeatherRepository, private val checklistRepository: ChecklistRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
when {
modelClass.isAssignableFrom(WeatherViewModel::class.java) ->
return WeatherViewModel(weatherRepository) as T
modelClass.isAssignableFrom(ChecklistViewModel::class.java) ->
return ChecklistViewModel(checklistRepository) as T
else ->
throw IllegalArgumentException("ViewModel Not Found")
}
}
}
class WeatherFragment : Fragment() {
private lateinit var mWeatherModel: WeatherViewModel
#Inject
internal lateinit var viewModelFactory: ViewModelProvider.Factory
....
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mWeatherModel = ViewModelProviders.of(this, viewModelFactory)
.get(WeatherViewModel::class.java)
...
}
}
It is not necessary to change your postValue to setValue since it is done in a same Thread. The real issue here is the way how Dagger2 is supposed to be set.
In WeatherFragment.kt use
internal lateinit var viewModelFactory: CustomViewModelFactory
rather than
internal lateinit var viewModelFactory: ViewModelProvider.Factory
It is also necessary to add #Inject annotation in your CustomViewModelFactory.kt's constructor.
class CustomViewModelFactory #Inject constructor(
And lastly your WeatherProvider.kt is not in initialized state at all base on the code you provided. You can do it using this code :
init {
getFromInternet()
}
Try to use
mDownloadedWeather.setValue(WeatherUtils.extractValues(weather))
instead of
mDownloadedWeather.postValue(WeatherUtils.extractValues(weather))
Because postValue() Posts a task to a main thread to set the given value. So if you have a following code executed in the main thread:
liveData.postValue("a");
liveData.setValue("b");
The value "b" would be set at first and later the main thread would override it with the value "a".
If you called this method multiple times before a main thread executed a posted task, only the last value would be dispatched.