Android: how to start dialog for the result
The main goal of this article is to demonstrate how to create the StartDialogForResult using different approaches, elaborate the topic about using dialogues in Android and explain the difference between all types of dialogues by using the task that I had a few days ago.
Page structure:
- Intro
- What the problem is?
- Final result
- Solutions
- Practice
- Conclusions
INTRO
Not so long ago, I was working on one interesting task at work and, I had to start Android’s Dialog to pick up some data from the list, it should have been something like StartActivityForResult but for Dialogs.
I was wondering how people managed issues like that. I was sure, that there would be a lot of libraries that work like plug and play with wonderful customization as it usually is. However, I didn’t find anything that would be available to re-use or implement as 3rd party library. That’s led me to think that probably people do it by other methods, either it’s just not relevant, or it’s technically wrong.
Since I’ve searched everything and found nothing I decided to share my version of solving this problem and write an article about how to create something that might be called StartDialogForResult.
WHAT THE PROBLEM IS?
Everything started with the development of the registration page for my current project, which was super complex for implementing it into the mobile screen.
It is worth noticing that we had a unique design that was sharpened for this registration. But, many of the UI elements simply could not be technically done or configured as required using standards that are provided by Android, from the default packages such as android.widget, android.appcompat, and android.material. I always had to invent tricky solutions based on a widget with the largest number of things that satisfied me by default by customizing others on top of it.
Since the page was very large and complex we decided to replace editable fields into Floating/Swipeable dialogues, which have done a great job both from a technical and a design point of view.
FINAL RESULT
The final result of creating StartDialogForResult with SlideUp animation that reacts on the finger looks this way:
SOLUTIONS
Android Development is an amazing thing that always provides a variety of options for us on how to solve the problem. It will be wrong to say that one solution is correct, and others are not.
No, in no case. It is important to understand that every solution is correct if it works, it is for us to decide which one fits into our eco-system better by comparing them to the architecture, behaviour, and seeing if it solves the problem for 100%, whether it’s independent and so on.
To solve the task with Dialogs, Android provides us with various options, let’s list them:
- By using Dialog class;
- By using AlertDialog class;
- Via DialogFragment;
- Or create a full-fledged fragment;
These are all different classes that work in different ways (it’s important to understand that now we are only talking about how to use them for this task, not about a general overview). Some of them were created exclusively to solve only one problem, another has limited functionality, and some can act as a full-fledged class and do almost everything. Let’s look at each of them, try to compare and see what is best for us.
Dialog vs AlertDialog
This is the base class for creating dialogues but implementing the Dialog class directly is not recommended. Instead, use one of the following subclasses: AlertDialog.
AlertDialog is a subclass of Dialog that can display only simple vies inside itself: text view, one, two or three buttons, etc.
AlertDialog is supposed to deal with informative matters only, That’s the reason why complex interaction with the user is limited. For instance, if you create an AlertDialog and add an EditText to it then when you click on EditText the keyboard does not open. For this to work, you just need to use Dialog, and this is not recommended.
The default usage of AlertDialog has the following code:
AlertDialog.Builder(this)
.setMessage("Please, confirm the action")
.setPositiveButton("Confirm") { dialog, which ->
// handle click
}
.setNegativeButton("Cancel", null)
.create()
.show()
This approach seems to be obvious and intuitive but like in many cases, when you are working with Android, it might be completely wrong.
Using such a simple code, we get a memory leak. Just rotate your smartphone and you will see an error in the console. When using DialogFragment or Fragment it might not happen (depends on the implementation).
And yet, especially in our case, if we use this approach all the code will be stored there and this will not solve the problem set for us to simplify the registration by dividing the code into small pieces.
Therefore, concluding the information above, Dialog and AlertDialog are not suitable for us. Is it DialogFragment that remains or just Fragment? What’s the difference? Which one to choose?
DialogFragment vs Fragment
DialogFragment is a utility class that extends the Fragment class.
Thus, DialogFragment is displayed inside the fragment. All information related to the Dialog will be stored and managed through the fragment. Since DialogFragment is closely related to Fragment and has its lifecycle, in our case, both lifecycles are suitable, but if you need to do something tricky or special, then better read the official information on how it works to understand it better. It turns out that you can use DialogFragment or Fragment. And that’s it? Technically, sort of, yes, but let’s take a step forward to understand how Fragment or DialogFragment will eventually communicate with our code.
Consider the case if we chose Fragment (instead of DialogFragment).
If you call a Fragment through your Activity you will receive the data you want back with one Callback, not a big deal. But if you want to use it through the Fragment, then you have to create 3 callbacks, which already sounds messy:
- Notify an Activity that a Fragment wants to create new Dialog;
- Return data from a Fragment to an Activity;
- Transfer data from an Activity to a fragment you triggered from;
It doesn’t look like I want to do such manipulation for the sake of simple tasks that eventually confuse us in our code.
Oh, I think I hear the screams of the RX developers, and you? Okay-okay, we have a few alternative options on how to transfer the data through the application/screens at least by using RX or ViewModels. I just wanted to say that it is not worth implementing such a big library for such a little purpose. If you already use it, of course, you can do it, why not?
Perhaps, because to create a fully-open object that might be called from any class isn’t a good idea and it breaks common patterns and principles such as the Single Responsibility Principle and makes the code difficult to read and difficult to maintain, let alone the pain in case of refactoring? But,
you develop — you decide.
Consider the second option with DialogFragment.
If you want to use it through the Activity it’s not a problem. You have one method that’s triggered when you need and returns the data.
Do you wanna call it directly from the Fragment? You have a unique implementation that works even from fragments. Just do it, it works. Everything is concise and intuitive. It’s also very important to note:
If you have MVVM architecture then you can reuse your ViewModels in DialogFragment, but this approach does not work with Presenters from MVP, you have to create a separate Presenter for this.
Since we have already completed the study of the theoretical part and realized what will be better for us, I suggest moving on to the practical part, which we all love so much.
PRACTICE
To make the StartDialogForResult we need the main page, which will contain one main fragment, the dialogue, and the interface that describes the behaviour and possibly some visual goodies (this is optional).
The code for the main page looks like this:
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)val fragment = MainFragment()
val fm: FragmentManager = supportFragmentManager
val transaction: FragmentTransaction = fm.beginTransaction()
transaction.addToBackStack(null)
transaction.replace(R.id.activity_main_container, fragment)
transaction.commit()
}
}
There is also nothing complicated in the XML file, LinearLayout is selected for the parent class, solely because of the ease of use. If you need any other — use whatever you want, there is no problem. Inside the LinearLayout we have the container which will be replaced with a needed Fragment:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity" ><FrameLayout
android:id="@+id/activity_main_container"
android:layout_width="match_parent"
android:layout_height="match_parent" /></LinearLayout>
Let’s create a fragment to complete the logical chain and be able to run the application without errors. Our Fragment will have only the EditText (non-editable, but clickable) inside, clicking on it will run the DialogFragment, where the business logic will be handled and the result returned. Fragment class has the following code:
class MainFragment : Fragment() {override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(
R.layout.fragment_main, container, false
)override fun onViewCreated(view: View, state: Bundle?) {
super.onViewCreated(view, state)
main_fragment_et.setOnClickListener {
// todo: implement dialog first.
}
}
}
The XML file of fragment also has a simple enough code because we use it only to show how things are going, the main UI/Code will be in DialogFragment.
MainFragment XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"> <EditText
android:id="@+id/fragment_main_et"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Location"
android:inputType="none"
android:focusable="false"
android:paddingBottom="12dp"
android:paddingTop="12dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:layout_marginTop="32dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:textAllCaps="false"
android:textColor="@android:color/black"
android:textColorHint="#838da2"
android:textCursorDrawable="@null"
android:textSize="14sp"
android:textStyle="normal"
android:background="@drawable/shape_round_border_hint"
/></LinearLayout>
All together looks like an empty page with one EditText:
After that, we can create the DialogFragment. But first, let’s think about “What kind of Dialog behaviour we might have?”, i.e. what can be expected to create a convenient basic behaviour for Dialog?
The simplest and most desirable option is when the user opens, selects and closes the dialogue with the data he wanted. In this case, all we need to do is to pass the selected into our callback.
Generally, something can go wrong. So a method for handling errors is always needed, and as a rule, it passes an error, so the programmer can understand what is wrong.
There also might be a “Neutral case”. For instance, the user opened and closed a dialogue manually, i.e. changed his mind. At first glance, it seems useless, but in some cases, it’s very important. For example, if you have some kind of experimental design and don’t know how users react to it.
You can implement some kind of tracker to understand how many people close it by themselves, and how many use the function, then you can see how much the design repels users. This is only an example, probably not the best one, but the main thing for it you had to understand.
Having put everything together, we can create an interface for the basic behaviour of the dialogue box, it looks like the following code:
interface DialogForResultCallback {
fun onResultSuccess(item: DisplayableItem)
fun onResultFailed(ex: Exception)
fun onResultNeutral(item: Any?)
}
This piece of code should be clear. The only thing you might be interested in is DisplayableItem.
DisplayableItem is an empty interface, I used to mark my POJO objects that’s might be parsed, transmitter, or mapped thought lambdas (This is only for different iterations inside adapters).
I use it as a default parameter to data transmitting when I work with adapters. It is very comfortable to work with base classes, adapters and so on. You only need to declare the behaviour for the data, and then you can reuse it for all models you want to. This class (DisplayableItem) is optional, you can replace it with a specific one.
Now, when we have enough knowledge of how dialogues work, let’s create the one (finally, ha?)
XML file of Dialog has the following code:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:orientation="vertical"> <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"> <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/dialog_select_address_iv_line"
android:layout_width="38dp"
android:layout_height="4dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:background="@drawable/shape_rounded_large"
android:backgroundTint="#D8DDE6"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" /> <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/dialog_select_address_iv_close"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:visibility="gone"
android:background="@drawable/ic_close"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/dialog_select_address_tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Select location"
android:textColor="#1f242e"
android:textSize="18sp"
android:textStyle="bold" /> <EditText
android:id="@+id/dialog_select_address_et_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_round_border_hint"
android:hint="Enter your location"
android:layout_margin="16dp"
android:padding="12dp"
android:textAllCaps="false"
android:textColor="#1f242e"
android:textColorHint="#838da2"
android:textCursorDrawable="@null"
android:textSize="14sp" /> <androidx.recyclerview.widget.RecyclerView
android:id="@+id/dialog_select_address_rv_suggestions"
android:layout_width="match_parent"
android:layout_height="match_parent" /></LinearLayout>
The Dialog contains white background at places when we want to make it transparent, and the view with a blueprint looks like this:
The dialogue will render the data, filter it, and respond to returning one. To reach this goal we need:
- to pass the callback into the constructor of the Dialog;
- to get the data for filtering, in our case I will put it in the constructor too, just to keep the example simple;
- to render the data with RecyclerView and his adapter;
- to return the result (selected item, exception or something neutral);
LocationDialog class has the following code:
class LocationDialog(
private val callback: DialogForResultCallback,
private val locations: List<Location>
) : BaseDialogFragment() { private lateinit var et: EditText private val locationAdapter by lazy {
LocationAdapter(click = ::onClickLocation)
} override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
dialog?.window?.attributes?.let {
windowAnimations = R.style.DialogAnimationSlideUp
}
} override fun onStart() {
super.onStart()
dialog?.window?.let {
it.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
it.setBackgroundDrawable(
ColorDrawable(Color.TRANSPARENT)
)
}
} override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
state: Bundle?
): View? {
val view = super.onCreateView(inflater, container, state)
if (dialog != null && dialog?.window != null) {
dialog?.window?.requestFeature(Window.FEATURE_NO_TITLE)
}
return view
}
override fun onCreateDialog(state: Bundle?): Dialog {
val builder = AlertDialog.Builder(activity)
val inflater = activity?.layoutInflater
val layout = inflater?.inflate(
R.layout.dialog_location, null
) layout?.let { view ->
val rv = view.findViewById<RecyclerView>(
R.id.dialog_select_address_rv_suggestions
)
et = view.findViewById(
R.id.dialog_select_address_et_input
)
et.requestFocus()
showKeyboard() with(rv) {
adapter = locationAdapter
layoutManager = LinearLayoutManager(
context, RecyclerView.VERTICAL, false
)
addItemDecoration(
LinearDecorator(
8.dp(), 16.dp(), 8.dp(), 16.dp()
)
)
} et.addTextChangedListener {
it?.let { query ->
val actualData = locations.filter { location ->
location.city.contains(query, true) ||
location.country.contains(query, true)
}
when (actualData) {
null -> callback.onResultFailed(
Exception(
"Error while fetching locations"
)
)
else -> locationAdapter.updateLocationData(
actualData
)
}
}
}
} builder.setCancelable(true)
builder.setView(layout)
return builder.create()
} private fun showKeyboard() {
val imm = context?.getSystemService(
Context.INPUT_METHOD_SERVICE
) as InputMethodManager?
imm?.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0)
} private fun hideKeyboard() {
val imm = context?.getSystemService(
Context.INPUT_METHOD_SERVICE
) as? InputMethodManager
imm?.hideSoftInputFromWindow(view?.windowToken, 0)
} private fun onClickLocation(item: DisplayableItem) {
et.clearFocus()
hideKeyboard()
callback.onResultSuccess(item as Location)
dismiss()
} companion object {
const val TAG = "LocationDialog"
}
}
I want to make some moments, that can be not so obvious, clear from the top to the bottom of the code in this class:
- In the adapter, we put lambda that responses for clicking on elements;
- In methods onActivityCreated() and onStart() we just make background transparent programmatically;
- In the method onCreateDialog, we call requestFocus() and showKeyboard() functions to open the keyboard and make EditText active. It is convenient.
However, everything else is quite simple and should be clear or easy to google. Just in case, I’ll show you the Adapter class. There is only handling and rendering list items inside Dialog:
class LocationAdapter(
val click: (DisplayableItem) -> Unit
) : RecyclerView.Adapter<LocationAdapter.LocationAdapterVH>() { private var locations: MutableList<Location> = mutableListOf() override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): LocationAdapterVH {
return LocationAdapterVH(LayoutInflater.from(parent.context)
.inflate(R.layout.item_location, parent, false)
)
} override fun onBindViewHolder(
holder: LocationAdapterVH,
position: Int
) {
val item = locations[position]
holder.tvTitle.text = item.city
holder.tvDesc.text = item.title
holder.parentLayout.setOnClickListener {
click(item)
}
} override fun getItemCount(): Int = locations.size fun updateLocationData(data: List<Location>) {
locations.clear()
locations.addAll(data)
notifyDataSetChanged()
}
inner class LocationAdapterVH(
itemView: View
) : RecyclerView.ViewHolder(itemView) {
val parentLayout: RelativeLayout = itemView.findViewById(
R.id.item_location_parent
)
val tvTitle: AppCompatTextView = itemView.findViewById(
R.id.item_location_tv_title
)
val tvDesc: AppCompatTextView = itemView.findViewById(
R.id.item_location_tv_subtitle
)
}
}
And XML file of the row item:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/item_location_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"> <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/item_location_iv_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/item_location_tv_title"
android:layout_alignBottom="@+id/item_location_tv_subtitle"
android:padding="8dp"
android:src="@drawable/ic_map_pin" /> <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/item_location_iv_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/item_location_tv_title"
android:layout_alignBottom="@+id/item_location_tv_subtitle"
android:layout_alignParentEnd="true"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:paddingStart="8dp"
android:paddingEnd="16dp"
android:src="@drawable/ic_chevron_right"
/> <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/item_location_tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginTop="-3dp"
android:layout_toStartOf="@+id/item_location_iv_right"
android:layout_toEndOf="@id/item_location_iv_left"
android:drawablePadding="16dp"
android:paddingStart="8dp"
android:paddingTop="8dp"
android:paddingEnd="8dp"
android:paddingBottom="0dp"
android:textAlignment="viewStart"
android:textColor="#1f242e"
android:textSize="15sp"
tools:text="Some text for example" /> <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/item_location_tv_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/item_location_tv_title"
android:layout_toStartOf="@+id/item_location_iv_right"
android:layout_toEndOf="@id/item_location_iv_left"
android:drawablePadding="16dp"
android:paddingStart="8dp"
android:paddingTop="0dp"
android:paddingEnd="8dp"
android:paddingBottom="8dp"
android:textAlignment="viewStart"
android:textColor="#7F8186"
android:textSize="12sp"
tools:text="Some text for example" /></RelativeLayout>
That’s all if you did everything step by step, then everything should work fine for you 🎉.
To improve UI/UX by making it more dynamic, I recommend replacing EditText for TextInputLayout plus TextInputEditText, this is the key. Also, I recommend connecting the library to slide up the menu by finger:
implementation "com.sothree.slidinguppanel:library:$version”
Look at the developer’s page for instructions on how to use it → here.
Hint: just wrap your UI inside a CoordinatorLayout and add these parameters to your inner Layout:
android:layout_gravity="bottom"
app:behavior_hideable="true"
app:behavior_peekHeight="0dp"
app:behavior_skipCollapsed="true"
app:layout_behavior= "com.google.android.material
.bottomsheet.BottomSheetBehavior"
And, the UI altogether has the following view:
You can find all the sources on GitHub on my repository:
CONCLUSIONS
While reading this article, we’ve learned a lot of things, for example:
- We dealt with all types of Dialogs in Android;
- Leaned on how to implement them;
- Found out the difference between them;
- Compared them on a practical task from the real everyday-life of an Android Developer;
- We analyzed and tried to predict the behaviour of the Dialog to create an interface that will handle it;
- We have strengthened all theoretical knowledge with practical code writing;
- And most importantly, we solved our task, which we were faced with: all the code was placed in a separate class, the dialogue box independently performs its functions.
More information you can find out on my Patreon and Youtube channel:
🎬 All the content that I make is all my personal experience, I create it by myself from scratch and share with you very useful information that no one tells you, so please support it by subscribing.
🙂 That’s all for today ;)
👉🏻 Press the like button, leave a comment below and subscribe!
If you have read up to this point, then know — you lit!
If I’m right, praise me; if I’m wrong, scold me.
But don’t for