Android alertdialog and exceptions - android

I have a fragment that asks for some input and then has a submit button. When the submit button on that page is hit it goes to a viewmodel routine.
submitButton.setOnClickListener {
viewModel.submitted(binding)
}
The viewModel.submitted routine checks the sanity of the values entered that are available in the binding value. If the sanity checks fail I want to show an AlertDialog. If everything is sane then viewModel.submitted adds a record to the database. I would like to show an AlertDialog to show a new record was created successfully.
I am following the instructions here https://developer.android.com/guide/topics/ui/dialogs. It says to create a DialogFragment that defines a AlertDialog. I modified the onClickListener lamda so that if there is a sanity check error, ViewModel.submitted throws an exception which is caught and shows an alertDialog.
submitButton.setOnClickListener {
try {
viewModel.submitted(binding)
} catch (e: Exception) {
val alert = AddSymbolDialogFragment()
alert.message = e.message.toString()
alert.show(parentFragmentManager, "Test1")
}
}
This worked for the checks in the viewModel.submitted() routine that were just checking the values in the binding that is passed to it. But in viewModel.submitted() I need to check some of the binding values against values in the database. So, inside viewModel.submitted() I launch a coroutine to get the database info to compare against the binding values. If I throw an exception inside the coroutine the app dies. This makes sense because the setOnClickListener() lambda function has already completed. So, there is nothing to catch it.
I have worked around this by passing the parentFormatManager to viewModel.submitted:
submitButton.setOnClickListener {
viewModel.submitted(binding, parentFragmentManager)
}
Note: the instruction page mentioned above in the "Showing a Dialog" section it says to use getFragmentManager from Fragment, but Android Studio says that is deprecated and to use parentFormatManager.
Then inside viewModel.submitted() I show the alert when I get an error either before the coroutine or after the coroutine:
val alert = AddSymbolDialogFragment()
alert.message = "bad Category"
alert.show(fragmentManager, "Test1")
That seems to work.
It is the viewModel.submitted() routine's job to update the data with the info in the binding parameter. When it is successful I will also want to show an AlertDialog just to show it was successful.
So, I have a setup that works. But, I am unsure it is the correct way to do it. It just doesn't feel right to have the viewModel show an alertDialog. Seems that ought to be done at the ui level.
Is there a more appropriate way to show alertDialogs rather than passing the Fragment's parentFragmentManager to the viewModel routine that performs the I/O work?
There is also kind of a bigger question here. When I launch a coroutine and an exception occurs in the coroutine how in general do you pass that exception up to higher levels of the code.
Thanks

Related

How to create a global progress dialog that can handle multiple asynchronous tasks

I have a base fragment where I added the method below that will show a progress dialog, on some occasions multiple async methods will start at the same time and all of them will be calling the showProgressDialog method, and so I'm facing the following problems:
the methods that call for showProgressDialog are not connected and not fixed, some of them might get called and some might not and they don't run in a specific order, depending on many user conditions.
if one of the methods that called the progress dialog failed it needs to show an error dialog, but other methods will stay running until they either finish or fail.
to solve the first problem I created a counter called showNo which will get incremented when a method wants to show the progress dialog, and decremented when the method finishes and wants to hide it, this way the progress dialog will get hidden only when all running methods finish. but this also cause another problem as it very hard to track and if you the method showing progress dialog doesn't get the correct showNo the progress dialog will stay on the screen for ever.
fun showProgressDialog(show: Boolean) {
if (!show && dialog != null) {
showNo--
if (showNo <= 0) {
dialog!!.dismiss()
showNo = 0
}
return
} else if (!show && dialog == null) {
return
}
if (showNo <= 0 && context != null) {
val binding: DialogProgressBinding =
DialogProgressBinding.inflate(LayoutInflater.from(requireContext()), null, false)
dialog = Dialog(requireContext())
dialog!!.apply {
setContentView(binding.root)
setCancelable(false)
window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dismiss()
show()
}
}
showNo++
}
this is the method I created to show the dialog, but I need a better approach that is more usable
You probably want to create a shared ViewModel that all your Fragments can access, say through your Activity's ViewModelProvider or one for your current navigation graph (if you're using the ktx version of the viewmodel library you can create these with val model: MyViewModel by activityViewModels() or by navGraphViewModels())
That way you can have one central component that's responsible for tracking the current state of what should be displayed, and UI components (say one of your Fragments, or the containing Activity) can observe a LiveData exposing that state, and display any dialogs as required.
It's probably a good idea to create an enum that represents the possible states:
enum class DisplayState {
NONE, PROGRESS, ERROR, PROGRESS_AND_ERROR
}
Or maybe a sealed class, so you can give the different states properties:
sealed class DisplayState {
data class Progress(val progressAmount: Int) : DisplayState()
data class Error(errorMessage: String) : DisplayState()
data class ProgressAndError(val progressAmount: Int, val errorMessage: String): DisplayState()
}
Or maybe just a combo state object:
// If a thing isn't null, display it!
// Null default values make it easy to say exactly what state you want,
// e.g. DisplayState(errorMessage = "oh nos") is just an error message
data class DisplayState(
val progressAmount: Int? = null,
val errorMessage: String? = null
)
And now you can expose that state in a VM:
class MyViewModel : ViewModel() {
// using the "combo" version above
private val _displayState = mutableLiveData(DisplayState())
// observe this to see the current state, and any updates to it
val displayState: LiveData<DisplayState> get() = _displayState
}
and the observer in the UI layer can show or hide dialogs as necessary.
Since everything's coordinated through this one ViewModel instance, it can keep track of things like how many clients have requested a particular dialog is shown. This part's a bit tricky because it depends what you want - if two different callers request a progress dialog at different times or with different amounts of progress, how much progress do you display? Does that change if the caller with the displayed progress cancels its dialog request?
If so, you'll need to keep track of which callers have ongoing requests and what their individual state (e.g. progress) is, e.g. by putting them in a Map. (You could use a WeakHashMap to avoid holding Fragments in memory, but I wouldn't recommend it - I'll get to that in a minute.) Otherwise, a counter is probably fine? Depending on how you're doing async stuff you might need some thread-safe synchronisation.
Here's one way you could do it, using caller references:
// in the VM
val progressDialogRequesters = mutableSetOf<Any>()
fun requestProgressDialog(caller: Any) {
progressDialogRequesters += caller
// you could be smarter here and e.g. only call it if the set was empty before
updateDisplayState()
}
fun cancelProgressDialog(caller: Any) {
progressDialogRequesters -= caller
// again you could do this only if the set is now empty
updateDisplayState()
}
// also similar error ones
private fun updateDisplayState() {
// here you want to push a new state depending on the current situation
// with -all- requests and any state they have (e.g. progress amounts).
// Just doing really simple "if there's a request, show it" logic here
_displayState.value = DisplayState(
progressAmount = if (progressDialogRequesters.isEmpty()) null else 50,
errorMessage = if (errorDialogRequesters.isEmpty()) null else "UH OH"
)
}
If you're doing it that way, you could have Sets holding references to Fragments etc, potentially keeping them in memory. Using weak references like a WeakHashMap can avoid that - but really, what you want is a robust system where you explicitly cancel requests when you don't need them anymore, either because some task has finished, or because the Fragment or whatever is being destroyed. You don't want to rely on the system cleaning up after you, if that makes sense - it can make it harder to track down why things aren't lining up, why your counts are higher than you expected, etc.
It's a little more complicated than that - it really depends how your individual tasks are running, if they're part of a Fragment (and die with it or its viewLifecycleScope, e.g. if the device is rotated) or if they're more independent. You'll want to store the task itself as its own reference, if you can. But this is another example of why you probably want to coordinate all this through the ViewModel, rather than running it as part of the UI Layer. If the VM starts all your tasks, it can keep track of what's running, what's finished and so on. The UI layer just sends events like "start this type of task" and displays the current state. I can only be really general here though because it depends what you're doing. Hope this helps you work something out!

How to get data from a MutableLiveData

I'm trying to get data from a MutableLiveData; however, it seems like something is wrong with the code, can you guys check for me please?
I can get the object, but I failed to add the object to a mutableList
properties = ArrayList()
propertyViewModel.propertyItemLiveData.observe(
viewLifecycleOwner,
Observer { propertyItems ->
for (property in propertyItems){
var p:Property = Property(property.id,property.address
,property.price,property.phone,property.lat,property.lon)
println(p)// i can display data
properties.add(p)//when i add to properties, the properties still null. Why?
}
}
)
if (properties.isEmpty()){
println("null")
}
The code in the observer will only run when propertyItemLiveData pushes a new value, or if it already has a value when you first observe it. But from the docs:
As soon as an app component is in the STARTED state, it receives the most recent value from the LiveData objects it’s observing. This only occurs if the LiveData object to be observed has been set.
So you won't actually get a value until your Activity or Fragment hits the onStart() callback, meaning your observer code won't run until then. If the code you posted is running earlier than that (say in onCreate), then what you're doing is:
creating an empty list
adding an observer that will add stuff to that list (but it won't run until later)
checking if the list is still empty (it definitely is)
Because of the observer pattern, where your code reacts to new data/events being pushed to it, whatever you need to do with that populated list should be part of the observer code. It should react to the new value and take action - update a list view, alert the user, start an API call, whatever
propertyViewModel.propertyItemLiveData.observe(viewLifecycleOwner) { propertyItems ->
// handle the propertyItems, add them to your list etc
// then do whatever needs to happen with the list, e.g. display it
updateDisplay(propertyList)
}
btw if Property is a data class and you're just copying all its data, you can add to your list like this:
properties.addAll(propertyItems.map { it.copy() })
// or propertyItems.map(Property::copy)
hello first of all in kotlin in general you have to use mutableList and the check of empty or any other instruction should inside the call back like this :
properties = mutableListOf<YourClass>()
propertyViewModel.propertyItemLiveData.observe(
viewLifecycleOwner,
Observer { propertyItems ->
for (property in propertyItems){
var p:Property = Property(property.id,property.address
,property.price,property.phone,property.lat,property.lon)
println(p)// i can display data
properties.add(p)//when i add to properties, the properties
}
if (properties.isEmpty()){
println("null")
}
}
)

RxJava Realm findFirstAsync() result is not loaded, findFirst() fires endlessly

Why does this code endlessly execute it's .subscribe(..) part? I assumed that event will be fired only once, when matching Content object will be found. But it starts over and over again.
realm.where(Content.class)
.equalTo("keyID", id)
.findFirst()
.<Content>asObservable()
.map(this::getPostFromContent)
.subscribe(post -> {
loadComments(post.getId());
});
And if I change to .findFirstAsync() it throws an exception:
"Can't access a row that hasn't been loaded, make sure the instance is loaded by calling RealmObject.isLoaded()"
I am using latest version of realm.
UPDATE
I was able to make this work by using:
realm.where(RealmWrappedContent.class)
.equalTo("keyID", id)
.findFirstAsync()
.<RealmWrappedContent>asObservable()
.filter(post -> post.isLoaded())
.first()
.map(this::getPostFromContent)
.subscribe(post -> {
loadComments(post.getId());
});
But that's way too many thing you need to write just to use realm objects as observables. Is there any better way?
The default behaviour is submitting each time there is an update. In that case you need to use filter/first to only get 1 item as you found out.
If you want to change that behaviour in your entire app, you can also provide your own RxObservableFactory implementation as described here: https://realm.io/docs/java/latest/#rxjava.
The default factory is called RealmObservableFactory and it should be fairly easy to either wrap that or provide your own implementation that does what your want for all observables.

How to handle errors in custom AsyncTaskLoader?

I am extending AsyncTaskLoader which I use later in a Fragment through LoaderManager. Is there suggested way to handle errors which occur during async task? I would like to popup message asking user to cancel or retry when internet connection error occurs.
Only way which I can think of now is to make Loader to return null on error or wrap loaded object into another which would have status code.
What we did in our last project: Create a Wrapper that holds a generic and an excpetion like this:
public class AsyncTaskResult<T> {
private final T result;
private final Exception error;
//getter & setter
}
Catch all Exceptions in your doInBackground(...) and pack them into the result wrapper (or the result if no error). In your UI check the wrapper if it's an exception, then show according error message, otherwise populate the fields with the result.
For us it was also good practice to define what unique types of exceptions, there are (e.g. exception with a recoverable error where you only show a dialog or an app failure where you need to kick the user to the main menu) and only throw these kinds (on catching the specific in your asynctask), so you don't have to bother with hundreds of different exceptions and also abstract your error handling. You could also provide String keys with the correct I18n error message so you only have to write e.getMessage()
I've seen good results when returning a composite object that contains the payload (if any) and a status code, as you suggested. Then the Fragment that's hosting the AsyncTaskLoader can display an appropriate and informative error. This approach has the added advantage that it uses the built-in loader lifecycle.
Another option is to register a listener that your AsyncTaskLoader will notify when errors occur. The Facebook SDK has an example of using error listeners with loaders.

Android - Testing a simple Activity method

i'm writing you today because i have a real and concrete question about how testing an activity method. Let me explain the situation.
I have an activity (A) which use a method save(param) from a service (S). This method throws a specific exception if the parameter is invalid.
Unit tests are written for this part, and now, i would like to test the Activity part :
In my Activty, i use the following code to call the save() method
public void OnSaveClicked()
{
try{
if ( S.save(my_object) > 0 ) // Object Saved
{
ShowToast(this, "Your object has been saved successfully !");
}
else { // Error occured with the database
ShowToast(this, "An error occured with the database");
}
catch (MyException ex)
{
ShowToast(this, "The object you are trying to save is not valid... Please check information you entered");
}
}
The test that i'm trying to write has to check if the save() method raised the exception or not. To do that, i simulate a click on the UI Button, but, i don't really know what do i have to check. I read that's not possible to write an assert on a toast message. I'm currently thinking about using a dialog but i am not sure that the way i'm thinking is the right one. Is it normal to test that or not ?
Would you give me your feedback about that, and discuss about the right way to test activity ?
Thanks,
t00f
You might want to look at Robolectric. They have support for assertions like this http://groups.google.com/group/robolectric/browse_thread/thread/87952e620ce1bb37?pli=1
Thanks,
Ian

Categories

Resources