I Got rarely happens Can't access ViewModels from onDestroy - android

I got an error like this after launch the app, and that very rarely happens.
Here my complete Java stack trace associated:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.mycompany.myapp.dev, PID: 11145
java.lang.IllegalStateException: Can't access ViewModels from onDestroy
at android.arch.lifecycle.HolderFragment$HolderFragmentManager.findHolderFragment(HolderFragment.java:136)
at android.arch.lifecycle.HolderFragment$HolderFragmentManager.holderFragmentFor(HolderFragment.java:155)
at android.arch.lifecycle.HolderFragment.holderFragmentFor(HolderFragment.java:81)
at android.arch.lifecycle.ViewModelStores.of(ViewModelStores.java:42)
at android.arch.lifecycle.ViewModelProviders.of(ViewModelProviders.java:88)
at com.mycompany.myapp.ui.splash.SplashActivity$viewModel$2.invoke(SplashActivity.kt:47)
at com.mycompany.myapp.ui.splash.SplashActivity$viewModel$2.invoke(SplashActivity.kt:40)
at kotlin.SynchronizedLazyImpl.getValue(Lazy.kt:130)
at com.mycompany.myapp.ui.splash.SplashActivity.getViewModel(SplashActivity.kt:0)
at com.mycompany.myapp.ui.splash.SplashActivity$onResume$1.run(SplashActivity.kt:62)
at android.os.Handler.handleCallback(Handler.java:815)
at android.os.Handler.dispatchMessage(Handler.java:104)
at android.os.Looper.loop(Looper.java:207)
at android.app.ActivityThread.main(ActivityThread.java:5805)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:789)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:679)
Here my code, but It's very weird because I'm not set on onDestroy()
class SplashActivity : AppCompatActivity(){
GoogleApiClient.OnConnectionFailedListener {
val context = this
val viewModel by lazy {
ViewModelProviders.of(this).get(ListModel::class.java)
}
override fun onResume() {
super.onResume()
Handler().postDelayed(object: Runnable{
override fun run() {
val testConnection = viewModel?.testConnectionLiveData()
viewModel?.testConnection()
testConnection?.observe(context, Observer<TestConnectionData> { tc ->
tc?.let {
if (tc.status == "FAILED") {
var builder: AlertDialog = AlertDialog.Builder(context).create()
builder.setTitle("Failed Connect To Server")
builder.setMessage("The app cannot reach on the server. Make sure your device connect the internet and try again!")
builder.setButton(AlertDialog.BUTTON_POSITIVE, "OK", object : DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface?, which: Int) {
var i = Intent(context, LoginActivity::class.java)
startActivity(i)
}
})
builder.setIcon(android.R.drawable.ic_dialog_alert)
builder.setCancelable(false)
builder.setCanceledOnTouchOutside(false)
builder.show()
} else if(tc.status == "SUCCESS"){
var i = Intent(context, LoginActivity::class.java)
startActivity(i)
}
}
})
}
}, 5000)
}
#SuppressLint("MissingPermission")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN)
setContentView(R.layout.activity_splash)
}
}
I try to run again on Android studio, and everything is working normally.
I want to know why can happening and how solve it?

Looking into the Android source code, it checks if the support FragmentManager is destroyed when trying to get a ViewModel.
Calling
ViewModelProviders.of(this).get(ListModel::class.java)
will get to
private static HolderFragment findHolderFragment(FragmentManager manager) {
if (manager.isDestroyed()) {
throw new IllegalStateException("Can't access ViewModels from onDestroy");
}
Which will throw the exception if the FragmentManager is destroyed. Take care when the Runnable code executes with respect to the app lifecycle.
To solve this, you can:
check for getSupportFragmentManager().isDestroyed() before using the ViewModel
surround with try catch and ignore if does not affect your normal flow

Related

android.util.AndroidRuntimeException: Animators may only be run on Looper threads

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) }
}
}
}

Android - kotlin.UninitializedPropertyAccessException: lateinit property homeActivityBinding has not been initialized

I have a BaseFragment class that is extended by all fragments in my app. And i have a HomeActivity class is a starting activity and also has some generic functionality in it.
Here is my HomeActivity code:
class HomeActivity : HomeActivityContract.View {
private val presenter: HomeActivityContract.Presenter by inject {
parametersOf(Schedulers.computation())
}
override val selectedWatchList: WatchlistItem
get() = homeActivityBinding.watchlistSpinner.selectedWatchList
override val watchlistItems: MutableList<WatchlistItem>
get() = homeActivityBinding.watchlistSpinner.watchlistItems
val bag = CompositeDisposable()
lateinit var homeActivityBinding: ActivityHomeBinding
override
fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (presenter.hasAccount) {
homeActivityBinding = ActivityHomeBinding.inflate(layoutInflater)
setContentView(homeActivityBinding.root)
Observables.combineLatest(
realtimeDatabaseService.enableMaintenancePageForPhone,
realtimeDatabaseService.maintenanceDate
) { isEnabled, _mMsg ->
if (isEnabled) {
homeActivityBinding.homeMaintenanceView.root.visibility = View.VISIBLE
homeActivityBinding.homeParentView.visibility = View.GONE
homeActivityBinding.homeMaintenanceView.maintenanceTV.text = getString(R.string.maintenance_msg, _mMsg)
} else {
homeActivityBinding.homeMaintenanceView.root.visibility = View.GONE
homeActivityBinding.homeParentView.visibility = View.VISIBLE
presenter.attachView(this)
presenter.dataProvider.getAccountUpdate()
.subscribeBy(onError = { it.printStackTrace() }, onNext = {
presenter.sendDeviceIdForCurrentAccount(isNotificationsEnabledInSettings)
}).disposeBy(lifecycle.disposers.onDestroy)
setSupportActionBar(homeActivityBinding.homeToolbar)
presenter.start()
}
}
.subscribe()
.disposeBy(lifecycle.disposers.onDestroy)
} else {
val intent = Intent(this, LoginActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
startActivity(intent)
finish()
}
}
fun hideNavigationMenu() {
homeActivityBinding.homeNavigationView.visibility = View.GONE /// This is where it says that homeActivityBinding is uninitialised.
}
BaseFragment code:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (isAdded)
parentFragmentManager.let { fragmentManager ->
takeIf { fragmentManager.backStackEntryCount >= 1 }?.let {
(activity as? HomeActivity)?.hideNavigationMenu()
}
}
}
So BaseFragment is calling hideNavigationMenu in HomeActivity.
Here is the crash report:
Caused by kotlin.UninitializedPropertyAccessException: lateinit property homeActivityBinding has not been initialized
at com.app.android.traderpro.etx.activities.homeActivity.HomeActivity.G5(HomeActivity.kt:7)
at com.app.android.traderpro.etx.activities.homeActivity.HomeActivity.hideNavigationMenu(HomeActivity.kt:404)
at com.app.android.traderpro.etx.fragments.BaseFragment.hideNavigationMenu(BaseFragment.kt:138)
at com.app.android.traderpro.etx.fragments.BaseFragment.onCreate(BaseFragment.kt:153)
at androidx.fragment.app.Fragment.performCreate(Fragment.java:3090)
at androidx.fragment.app.FragmentStateManager.create(FragmentStateManager.java:475)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:257)
at androidx.fragment.app.FragmentStore.moveToExpectedState(FragmentStore.java:113)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1424)
at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2968)
at androidx.fragment.app.FragmentManager.dispatchCreate(FragmentManager.java:2875)
at androidx.fragment.app.FragmentController.dispatchCreate(FragmentController.java:252)
at androidx.fragment.app.FragmentActivity.onCreate(FragmentActivity.java:220)
at com.app.android.traderpro.etx.activities.BaseActivity.onCreate(BaseActivity.kt:61)
at com.app.android.traderpro.etx.activities.homeActivity.HomeActivity.onCreate(HomeActivity.kt:134)
at android.app.Activity.performCreate(Activity.java:8207)
at android.app.Activity.performCreate(Activity.java:8191)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3819)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4022)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2336)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:246)
at android.app.ActivityThread.main(ActivityThread.java:8653)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)
The first thing is I cannot replicate this crash and there are around 200 crashes reported in Firebase Crashlytics.
And
HomeActivity is the first activity that launches all other fragments. This crash is not happening as soon as this activity is started, it is happening after some time. So I don't understand how homeActivityBinding can be uninitialized.
I'd appreciate it if anyone can tell me how a lateinit property that is initialised can be uninitialized again
Fragment.onCreate() is too early to be trying to access members of the Activity, even though it is already attached. From the onCreate() documentation:
Note that this can be called while the fragment's activity is still in the process of being created. As such, you can not rely on things like the activity's content view hierarchy being initialized at this point.
You should move your code from onCreate to onViewCreated.

Viewmodel crash after on onactivityresult, Can not perform this action after onSaveInstanceState

I am trying a simple flow, from my activity I open the file picker then to viewmodel and this gives me a the following crash:
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
And here is the code that creates this crash:
private val mainViewModel: MainViewModel by viewModels()
private var activityResultLaunch = registerForActivityResult(StartActivityForResult()) { result ->
val fileUri = result.data?.data ?: return#registerForActivityResult
val stream = contentResolver.openInputStream(fileUri) ?: return#registerForActivityResult
uploadFiles(stream)
}
private fun uploadFiles(stream: InputStream) {
lifecycleScope.launchWhenStarted {
mainViewModel.uploadFiles(
stream = stream
).asLiveData().observe(this#MainActivity, {
handleFileUploadStatus(it)
})
}
}
#HiltViewModel
class MainViewModel #Inject constructor(
private val filesRepository: FilesRepository
) : ViewModel() {
suspend fun uploadFiles(stream: InputStream): Flow<UploadStatusUI> {
return filesRepository.uploadFiles(listOf(stream))
}
}
After some research I found this issue on Google's issue tracker here and another issue on firefox's github here but nothing worked. Apparently the issue got fixed on lifecycle 2.3.1 but I am still facing it.
Here are my versions:
"androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"
"androidx.lifecycle:lifecycle-viewmodel-savedstate:2.4.0"
Thanks in advance!
Edit: Crash stack trace here
Per the issue you linked to, other people continue to have the same problem, so maybe there's a bug in that library. Maybe try a work around. Instead of doing all that work in the activity:
private fun uploadFiles(stream: InputStream) {
lifecycleScope.launchWhenStarted {
mainViewModel.uploadFiles(
stream = stream
).asLiveData().observe(this#MainActivity, {
handleFileUploadStatus(it)
})
}
}
Break it up:
First, a live data object in the VM:
private val _uploadStatusLiveData = MutableLiveData<UploadStatusUI>()
val uploadStatusLiveData: LiveData<UploadStatusUI> get() = _uploadStatusUI
Next change your VM suspend function to do launch the coroutine:
// Not suspend, no return type - instead collect repo flow and pass to live data
fun uploadFiles(stream: InputStream) {
viewModelScope.launch(Dispatchers.IO) {
filesRepository.uploadFiles(listOf(stream)).collect {
_uploadStatusLiveData.postValue(it)
}
}
}
Finally, observe the changes in activity:
fun onCreate(...) {
viewModel.uploadStatusLiveData.observe(this) {
handleUploadStatus(it)
}
}
And manually start the process:
fun onStart(...) {
viewModel.uploadFiles(stream)
}

Automatically show/dismiss alert dialog in Kotlin with a bool

I am creating a text-to-speech app and want a dialog to be displayed to the speaker when the tts object is speaking and automatically hide itself when the finished. Anybody got a way to do this? Below is where I'm at so far, any ideas?
private lateinit var textToSpeech: TextToSpeech
private lateinit var alertDialogBuilder: AlertDialog.Builder
class MainActivity : AppCompatActivity() {
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
alertDialogBuilder = AlertDialog.Builder(this)
textToSpeech = TextToSpeech(applicationContext,
TextToSpeech.OnInitListener {})
}
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun speakToMe(view: View) {
alertDialogBuilder.setMessage("I'm speaking!")
alertDialogBuilder.show()
val charSeq = "Well hello there!" as CharSequence
textToSpeech.speak(charSeq, TextToSpeech.QUEUE_FLUSH, null, "")
while (!textToSpeech.isSpeaking){
// alertDialogBuilder.dismiss() or some other fun I can't seem to find
}
}
}
You can't use while in this way from the main thread because it will lock up the UI and you can get an ANR (application not responding crash).
When you start the speech, give it an ID, and then add a listener to respond to the result Since the listener might be called on another thread, you have to use runOnUiThread or a coroutine to go back to the main thread to manipulate your UI again.
When you call dialogBuilder.show(), store the returned AlertDialog so you can close it later.
By the way, there is no reason to cast the String to a CharSequence. You can just pass it to the function and it's recognized as a CharSequence by the compiler.
fun speakToMe(view: View) {
alertDialogBuilder.setMessage("I'm speaking!")
val dialog = alertDialogBuilder.show()
val charSeq = "Well hello there!"
val id = "speechId"
textToSpeech.speak(charSeq, TextToSpeech.QUEUE_FLUSH, null, id)
textToSpeech.onUtteranceProgressListener = object: UtteranceProgressListener {
override fun onDone(utteranceId: String) {
if (id == utteranceId) {
runOnUiThread { dialog.dismiss() }
}
}
}
}
You need to add a progress listener to your tts like so:
val params = Bundle()
params.putString(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, text)
tts.setOnUtteranceProgressListener(object : UtteranceProgressListener(){
override fun onDone(p0: String?) {
//dismiss the AlertDialog here (has to run on UI Thread)
}
override fun onError(p0: String?) {
}
override fun onStart(p0: String?) {
//show the AlertDialog here (has to run on UI Thread)
}
})
tts.speak(text ,TextToSpeech.QUEUE_FLUSH, params,"UtterID")
BEAR IN MIND, the "UtterID" (which can be anything you want) is essential to the functionality of the listener.

Kotlin wait for async with coroutine

I would like to open a new activity when phoneViewModel and ScanViewModel are instantiated. They are instantiated by calling an async function InitialRead(). I'm logging each step, atm they are logged as done3 => done2 => done1
I would like to have them in this order:
done1 => done2 => done3
I have following code:
class MainBusinessActivity : AppCompatActivity() {
private lateinit var scanViewModel: ScanViewModel
private lateinit var phoneViewModel: PhoneViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_business)
}
private fun startEntitySetListActivity() = GlobalScope.async {
val sapServiceManager = (application as SAPWizardApplication).sapServiceManager
sapServiceManager?.openODataStore {
phoneViewModel = ViewModelProvider(this#MainBusinessActivity).get(PhoneViewModel::class.java).also {it.initialRead{Log.e("done", "done1")}}
scanViewModel = ViewModelProvider(this#MainBusinessActivity).get(ScanViewModel::class.java).also {it.initialRead{Log.e("done", "done2")}}
}
}
override fun onResume() {
super.onResume()
//startEntitySetListActivity()
runBlocking {
startEntitySetListActivity().await()
val intent = Intent(this#MainBusinessActivity, HomeActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
Log.e("done", "done3")
startActivity(intent)
}
}
}
What am I doing wrong? Can someone correct my code?
Never use runBlocking in an Android app. runBlocking completely defeats the purpose of using coroutines, and can lead to an ANR. You also probably should never use GlobalScope, which leads to UI leaks. You might possibly need it for some kind of long-running task that doesn't make sense to put in a service but doesn't have dependency on any UI components, but I can't think of any examples
You also shouldn't be instantiating your ViewModels in the background. That should be done in onCreate().
Make this function a suspend function, and it can break down the two tasks in the background simultaneously before returning.
Start your coroutine with lifecycleScope.
Assuming sapServiceManager?.openODataStore is an asynchronous task that takes a callback, you will need to wrap it in suspendCoroutine.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_business)
phoneViewModel = ViewModelProvider(this#MainBusinessActivity).get(PhoneViewModel::class.java)
scanViewModel = ViewModelProvider(this#MainBusinessActivity).get(ScanViewModel::class.java)
}
private suspend fun startEntitySetListActivity() = coroutineScope {
val sapServiceManager = (application as SAPWizardApplication).sapServiceManager
sapServiceManager ?: return
suspendCoroutine<Unit> { continuation ->
sapServiceManager.openODataStore { continuation.resume(Unit) }
}
listOf(
launch {
phoneViewModel.initialRead{Log.e("done", "done1")}
},
launch {
scanViewModel.initialRead{Log.e("done", "done2")}
}
).joinAll()
}
override fun onResume() {
super.onResume()
lifecycleScope.launch {
startEntitySetListActivity()
val intent = Intent(this#MainBusinessActivity, HomeActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
Log.e("done", "done3")
startActivity(intent)
}
}

Categories

Resources