I have used viewLifecycleOwner.lifecycleScope and also tried converted uploadPhoto StateFlow to LiveData but still my App crashes at dismiss loadingDialogFragment
LoadingDialogFragment.kt
class LoadingDialogFragment : DialogFragment() {
private lateinit var mBinding: DialogLoaderBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, R.style.FullScreenDialogStyle)
isCancelable = false
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
mBinding = DialogLoaderBinding.inflate(inflater, container, false)
return mBinding.root
}
override fun show(manager: FragmentManager, tag: String?) {
///super.show(manager, tag)
try {
val ft: FragmentTransaction = manager.beginTransaction()
ft.add(this, tag).addToBackStack(null)
ft.commitAllowingStateLoss()
} catch (e: IllegalStateException) {
Log.e("IllegalStateException", "Exception", e)
}
}
}
ProfileFragment -> onViewCreated
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
mViewModel.uploadPhoto.collectLatest {
if (it.isLoading) {
loadingDialogFragment = LoadingDialogFragment()
loadingDialogFragment?.show(childFragmentManager, "loadingDialogFragment")
}
it.error?.let {
loadingDialogFragment?.dismiss() //crashed at here
Toast.makeText(requireContext(), "No ", Toast.LENGTH_SHORT)
.show()
}
it.data?.let {
loadingDialogFragment?.dismiss() //crashed at here
}
}
}
As far as my understanding is concern collectLatest code should not work when app activity or fragment is in onPause|onStop or onDestroy state.
I have faced this similar issue as well
I have solved this issue by using
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
// {code to collect from viewModel}
}
}
So , your code would be look like
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED){
mViewModel.uploadPhoto.collectLatest {
if (it.isLoading) {
loadingDialogFragment = LoadingDialogFragment()
loadingDialogFragment?.show(childFragmentManager, "loadingDialogFragment")
}
it.error?.let {
loadingDialogFragment?.dismiss()
Toast.makeText(requireContext(), "No ", Toast.LENGTH_SHORT)
.show()
}
it.data?.let {
loadingDialogFragment?.dismiss()
}
}
}
}
How it works
repeatOnLifecycle is a suspend function. As such, it needs to be executed within a coroutine. repeatOnLifecycle suspends the calling coroutine, and then runs a given suspend block that you pass as a parameter in a new coroutine each time the given lifecycle reaches a target state or higher. If the lifecycle state falls below the target, the coroutine launched for the block is cancelled. Lastly, the repeatOnLifecycle function itself won’t resume the calling coroutine until the lifecycle is DESTROYED.
Related
I have this code in which I am trying to observe a variable from my viewmodel. However, whenever I observe the variable, it always returns false, which is the default value, even though it should be returning true. I don't understand why it's not working, any idea and advice would be great.
This is the viewmodel part:
val isSuccessful = MutableLiveData(false)
fun acceptAgreement() = currentAgreement.value?.let {
viewModelScope.launch {
runCatching { agreementsRepository.acceptAgreement(it.id) }
.onSuccess { isSuccessful.postValue(true) }
.onFailure { isSuccessful.postValue(false) }
}
}
The observation in the fragment, where it always returns the showError():
binding.btnAccept.setOnClickListener { onAccept().also { continue()} }
private fun onAccept() = viewModel.acceptAgreement()
private fun continue() {
viewModel.isSuccessful.observe(viewLifecycleOwner, {
if (it) { start() } else { showError() }
})
}
Repository:
suspend fun acceptAgreement(id: String) = changeAgreement(id, status.ACCEPTED)
private suspend fun changeAgreement(id: String, status: status) {
try { agreementsService.changeAgreement(id, status.serialize()) }
catch (e: Throwable) { logger.error(this::class.java.name, "Failed to change status ${id}", e) }
}
Is there a reason you are running continue() after your run onAccept?
I believe what is happening is you haven't set the observer before you are observing.
So your flow goes:
onAccept -> triggers the update of the livedata.
Continue -> Sets the observer of the livedata.
I would suggest that you move the method call "continue()" into your onCreateView method of the fragment. It won't be triggered until it changes state in the viewmodel anyway.
Also you need to check you have set the viewLifecycleOwner of the fragment.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = FragmentYourFragmentNameBinding.inflate(inflater, container, false).apply {
lifecycleOwner = viewLifecycleOwner
}
continue()
return binding.root
}
isSuccessful.postValue(
runCatching { agreementsRepository() }.isSuccess
)
Instead of using isSuccessful.postValue() use isSuccessful.value = true. I have found that assignment, not the postValue method, updates registered observers for LiveData.
I am trying to stop my app from crashing when trying to read json information using Gson and Retrofit2 when user has no internet connection. Any Ideas?
My code:
Fragment
class ManuFragment : Fragment() {
private lateinit var viewModel: ManuFragmentVM
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentManuBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(ManuFragmentVM::class.java)
viewModel.apply {
manufacturer.observe(requireActivity(), {loadRecyclerView(it)})
setup()
}
}
View Model
class ManuFragmentVM : ViewModel() {
val manufacturer = MutableLiveData<List<Manufacturers>>()
fun setup() {
viewModelScope.launch(Dispatchers.Default) {
manufacturer.postValue(CarsRepository().getAllManufacturers())
}
}
Interface
interface ManufacturerApi {
#GET("url.json")
suspend fun fetchAllManufacturers(): List<Manufacturers>
}
Repository
class CarsRepository {
private fun manufacturerRetrofit(): ManufacturerApi {
return Retrofit.Builder()
.baseUrl("https://website.com/")
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
.build()
.create(ManufacturerApi::class.java)
}
suspend fun getAllManufacturers(): List<Manufacturers> {
return manufacturerRetrofit().fetchAllManufacturers()
}
It looks like you only need to wrap your call in a try-catch:
fun setup() {
viewModelScope.launch(Dispatchers.IO) {
try {
manufacturer.postValue(CarsRepository().getAllManufacturers())
} catch (ce: CancellationException) {
throw ce // Needed for coroutine scope cancellation
} catch (e: Exception) {
// display error
}
}
}
Also, you should run API calls explictly with the IO dispatcher. The rest of your code looks really good.
You can wrap manufacturer.postValue(CarsRepository().getAllManufacturers()) with try catch block. If you want to tell user about that error, create another LiveData object, e.g. val error: MutableLiveData<String> and post error text from catch block.
I am using StateFlow in my app and in my Fragment I use this to -
private var job: Job? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
job = lifecycleScope.launchWhenResumed {
viewModel.getData().collect {
// ...
}
}
}
override fun onPause() {
job?.cancel()
super.onPause()
}
As you see I cancel the job in onPause. How could I use a generalized function so that I can avoid doing the job?.cancel in every fragment.
I prefer not to use a BaseFragment
A simple solution would be to utilize the fragments lifecycle to automatically cancel the job when it is paused.
fun CoroutineScope.launchUntilPaused(lifecycleOwner: LifecycleOwner, block: suspend CoroutineScope.() -> Unit){
val job = launch(block = block)
lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onPause(owner: LifecycleOwner) {
job.cancel()
lifecycleOwner.lifecycle.removeObserver(this)
}
})
}
//Usage
class MyFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launchUntilPaused(this){
someFlow.collect{
...
}
}
}
}
If you have many of these jobs per fragment, I would advice to use a custom CoroutineScope instead, to avoid having many lifecycle observers active.
class CancelOnPauseScope(lifecycleOwner: LifecycleOwner): CoroutineScope by MainScope(){
init{
lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver{
override fun onPause(owner: LifecycleOwner) {
cancel()
lifecycleOwner.lifecycle.removeObserver(this)
}
})
}
}
class MyFragment: Fragment() {
private val scope = CancelOnPauseScope(this)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
scope.launch{
someFlow.collect{
...
}
}
}
}
a new way:
// Start a coroutine in the lifecycle scope
lifecycleScope.launch {
// repeatOnLifecycle launches the block in a new coroutine every time the
// lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
repeatOnLifecycle(Lifecycle.State.STARTED) {
}
}
public class AnyOfYourFragments extends AbsractFragment{
//you do here what you want to do
}
And in your AbstractFragment:
public abstract class AbstractFragment extends Fragment{
private var job: Job? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
job = lifecycleScope.launchWhenResumed {
viewModel.getData().collect {
// ...
}
}
}
override fun onPause() {
job?.cancel()
super.onPause()
}
}
I don't know kotlin, so my code is kind of mix with java but i'm sure you got the idea
I am using live data from a shared ViewModel across multiple fragments. I have a sign-in fragment which takes user's phone number and password and then the user presses sign in button I am calling the API for that, now if the sign-in fails I am showing a toast "Sign In failed", now if the user goes to "ForgotPassword" screen which also uses the same view model as "SignInFragment" and presses back from the forgot password screen, it comes to sign-in fragment, but it again shows the toast "Sign In failed" but the API is not called, it gets data from the previously registered observer, so is there any way to fix this?
SignInFragment.kt
class SignInFragment : Fragment() {
private lateinit var binding: FragmentSignInBinding
//Shared view model across two fragments
private val onBoardViewModel by activityViewModels<OnBoardViewModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_sign_in,
container,
false
)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
//This is calling again after coming back from new fragment it.
showToast("Sign In Failed")
}
}
override fun onClick(v: View?) {
when (v?.id!!) {
R.id.forgotPasswordTV -> {
findNavController().navigate(SignInFragmentDirections.actionSignInFragmentToForgotPasswordFragment())
}
R.id.signInTV -> {
val phoneNumber = binding.phoneNumberET.text
val password = binding.passwordET.text
val signInRequestModel = SignInRequestModel(
phoneNumber.toString(),
password.toString(),
""
)
//Calling API for the sign-in
onBoardViewModel.callSignInAPI(signInRequestModel)
}
}
}
}
ForgotPasswordFragment
class ForgotPasswordFragment : Fragment() {
private lateinit var binding: FragmentForgotPasswordBinding
//Shared view model across two fragments
private val onBoardViewModel by activityViewModels<OnBoardViewModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_forgot_password,
container,
false
)
return binding.root
}
}
OnBoardViewModel
class OnBoardViewModel : ViewModel() {
private var repository: OnBoardRepository = OnBoardRepository.getInstance()
private val signInRequestLiveData = MutableLiveData<SignInRequestModel>()
//Observing this data in sign in fragment
val signInResponse: LiveData<APIResource<SignInResponse>> =
signInRequestLiveData.switchMap {
repository.callSignInAPI(it)
}
//Calling this function from sign in fragment
fun callSignInAPI(signInRequestModel: SignInRequestModel) {
signInRequestLiveData.value = signInRequestModel
}
override fun onCleared() {
super.onCleared()
repository.clearRepo()
}
}
I have tried to move this code inside onActivityCreated but it's still getting called after coming back from new fragment.
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
showToast("Sign In Failed")
}
Using SingleLiveEvent class instead of LiveData in OnBoardViewModel class will solve your problem:
val signInResponse: SingleLiveEvent <APIResource<SignInResponse>>.
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, Observer<T> { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
override fun setValue(t: T?) {
pending.set(true)
super.setValue(t)
}
fun call() {
postValue(null)
}
}
This is a lifecycle-aware observable that sends only new updates after subscription. This LiveData only calls the observable if there's an explicit call to setValue() or call().
I would provide a way to reset your live data. Give it a nullable type. Your observers can ignore it when they get a null value. Call this function when you receive login data, so you also won't be repeating messages on a screen rotation.
class OnBoardViewModel : ViewModel() {
// ...
fun consumeSignInResponse() {
signInRequestLiveData.value = null
}
}
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
if (response != null) {
showToast("Sign In Failed")
onBoardViewModel.consumeSignInResponse()
}
}
For Kotlin users #Sergey answer can also be implemented using delegates like below
class SingleLiveEvent<T> : MutableLiveData<T>() {
var curUser: Boolean by Delegates.vetoable(false) { property, oldValue, newValue ->
newValue != oldValue
}
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, Observer<T> { t ->
if (curUser) {
observer.onChanged(t)
curUser = false
}
})
}
override fun setValue(t: T?) {
curUser = true
super.setValue(t)
}
fun call() {
postValue(null)
}
}
None of the other instances of this question are solving my problem. I have a Fragment that appears at the end of a transaction sequence. It is meant to close the app when a Timer contained within it completes:
var terminalTimer: Timer? = null
class TerminalFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_terminal, container, false)
}
override fun onStart() {
super.onStart()
initUi()
startCountDown()
}
override fun onStop() {
super.onStop()
AppLog.i(TAG, "onStop()")
stopCountDown()
}
private fun startCountDown() {
if (terminalTimer == null) {
terminalTimer = Timer()
terminalTimer!!.schedule(object : TimerTask() {
override fun run() {
AppLog.i(TAG, "Terminal countdown finished")
{
activity?.finish()
}
}, 5000)
}
}
private fun stopCountDown() {
AppLog.i(TAG, "stopCountDown()")
terminalTimer?.cancel()
terminalTimer?.purge()
terminalTimer = null
}
private fun returnToStart() {
AppLog.i(TAG, "returnToStart()")
(context as MainActivity).restartFlow() // calls popBackStackImmediate() for every fragment in the backstack, returning user to the beginning of their flow
}
companion object {
#JvmStatic
fun newInstance(terminalType: String, amountLoaded: Double) =
TerminalFragment().apply {
arguments = Bundle().apply {
}
}
}
}
stopCountDown() is being called whenever the fragment is navigated away from, but it somehow survives sometimes and closes the app from another Fragment. Using logs, I've also discovered that there appears to be 2 instances of this timer sometimes. How do I insure that this countdown is never active outside of this fragment and is cancelled/ destroyed in the Fragment's onStop()?