Mastering Custom Views in Android_ Reusable and Efficient Solutions

iFarmer Tech Team, ০১ অক্টোবর

5 মিনিটের পড়া

Mastering Custom Views in Android_ Reusable and Efficient Solutions

At iFarmer, we often need to build custom views for our mobile applications, which can be a time-consuming process. To accelerate development and enhance code reusability, we strive to create these views as modules. 

Today, we will learn how to create a custom dialog using the singleton pattern and reuse it throughout the app without having to recreate it each time. While there are many libraries available on GitHub for this purpose, modifying these libraries to fit your needs may require maintaining them separately, and they could eventually become incompatible with the latest Java or Kotlin versions.

Design your own view

Now simply design your view which you want to show in your dialog. Here is the example 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"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:background="@android:color/transparent"
  android:backgroundTint="@android:color/transparent">

  <androidx.cardview.widget.CardView
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_gravity="center"
      app:cardCornerRadius="14dp"
      app:cardElevation="2dp"
      app:cardUseCompatPadding="true">

      <LinearLayout
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:orientation="vertical">

          <ImageView
              android:id="@+id/ivImage"
              android:layout_width="80dp"
              android:layout_height="80dp"
              android:layout_gravity="center"
              android:layout_marginTop="@dimen/margin_15"
              android:layout_marginBottom="@dimen/margin_15"
              android:contentDescription="@string/app_name"
              android:src="@drawable/ic_wrong" />

          <TextView
              android:id="@+id/tvTitle"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:layout_marginTop="@dimen/margin_10"
              android:layout_marginBottom="@dimen/margin_10"
              android:gravity="center"
              android:text="Title"
              android:textAppearance="@style/TextAppearance.Bold.700"
              android:textColor="@android:color/black"
              android:textSize="20sp"
              android:textStyle="bold" />

          <TextView
              android:id="@+id/tvDescText"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:layout_marginStart="@dimen/margin_20"
              android:layout_marginTop="@dimen/margin_10"
              android:layout_marginEnd="@dimen/margin_20"
              android:layout_marginBottom="@dimen/margin_10"
              android:gravity="center"
              android:textColor="@android:color/black"
              android:textSize="@dimen/font_size_medium" />


          <LinearLayout
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:layout_marginLeft="@dimen/margin_10"
              android:layout_marginRight="@dimen/margin_10"
              android:orientation="vertical">

              <Button
                  android:id="@+id/btnAnotherWay"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  android:layout_marginStart="@dimen/margin_5"
                  android:layout_marginTop="@dimen/margin_10"
                  android:layout_marginEnd="@dimen/margin_5"
                  android:layout_marginBottom="20dp"
                  android:text="Try Again"
                  android:textAllCaps="false"
                  app:iconGravity="textStart" />

              <com.google.android.material.button.MaterialButton
                  android:id="@+id/btnTryAgain"
                  style="@style/ButtonOutlineAsh"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  android:layout_marginStart="@dimen/margin_5"
                  android:layout_marginEnd="@dimen/margin_5"
                  android:layout_marginBottom="25dp"
                  android:gravity="center"
                  android:text="Go Back"
                  android:textAlignment="center"
                  android:textAllCaps="false"
                  android:textAppearance="@style/TextAppearance.Normal"
                  android:textColor="@color/black"
                  android:textSize="14sp"
                  android:textStyle="normal"
                  app:backgroundTint="@color/gray3"
                  app:cornerRadius="50dp"
                  app:strokeWidth="0dp"
                  app:icon="@drawable/ic_arrow_left_black"
                  app:iconGravity="textStart"
                  app:iconPadding="10dp"
                  app:iconTint="@color/black" />

          </LinearLayout>
      </LinearLayout>
  </androidx.cardview.widget.CardView>
</LinearLayout>

 

This will show something like below:

 

Make it Functional:

Next, create a Java class to manage the functionality of this custom view. We'll name it DialogUtil. Here's the complete class:

 

public class DialogUtil {
  private static DialogUtil dialogUtil;
  private ContentBinding binding;
  private Dialog dialog;
  private Context context;

  public static DialogUtil instance(Context context) {

      if (dialogUtil == null) {
          dialogUtil = new DialogUtil();
      }
      dialogUtil.context = context;
      dialogUtil.binding = ContentBinding.inflate(LayoutInflater.from(context));
      dialogUtil.binding.btnAnotherWay.setVisibility(View.GONE);
      dialogUtil.binding.btnTryAgain.setVisibility(View.GONE);
      dialogUtil.binding.tvTitle.setVisibility(View.GONE);
      dialogUtil.binding.tvDescText.setVisibility(View.GONE);
      dialogUtil.binding.ivImage.setVisibility(View.GONE);


      dialogUtil.dialog = new Dialog(context);
      dialogUtil.dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
      dialogUtil.dialog.setContentView(dialogUtil.binding.getRoot());

      if (dialogUtil.dialog.getWindow() != null) {
          dialogUtil.dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
      }

      return dialogUtil;
  }

  public DialogUtil setPositiveButton(String text, DialogButtonClickLister dialogButtonClickLister) {
      binding.btnAnotherWay.setVisibility(View.VISIBLE);
      binding.btnAnotherWay.setText(text == null ? "" : text);
      binding.btnAnotherWay.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
              if(Constants.dblClck()){
                  return;
              }
              dialogButtonClickLister.onClick(view, dialog);
          }
      });
      return dialogUtil;
  }

  public DialogUtil setPositiveButton(int text, DialogButtonClickLister dialogButtonClickLister) {
      binding.btnAnotherWay.setVisibility(View.VISIBLE);
      binding.btnAnotherWay.setText(binding.getRoot().getResources().getString(text));
      binding.btnAnotherWay.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
              dialogButtonClickLister.onClick(view, dialog);
          }
      });
      return dialogUtil;
  }

  public DialogUtil setPositiveButton(String text, Drawable drawableLeft, DialogButtonClickLister dialogButtonClickLister) {
      ((MaterialButton)binding.btnAnotherWay).setIcon(drawableLeft);
      ((MaterialButton)binding.btnAnotherWay).setIconGravity(MaterialButton.ICON_GRAVITY_TEXT_START);
      binding.btnAnotherWay.setVisibility(View.VISIBLE);
      binding.btnAnotherWay.setText(text == null ? "" : text);
      binding.btnAnotherWay.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
              dialogButtonClickLister.onClick(view, dialog);
          }
      });
      return dialogUtil;
  }

  public DialogUtil setPositiveButton(int text, Drawable drawableLeft, DialogButtonClickLister dialogButtonClickLister) {
      ((MaterialButton)binding.btnAnotherWay).setIcon(drawableLeft);
      ((MaterialButton)binding.btnAnotherWay).setIconGravity(MaterialButton.ICON_GRAVITY_TEXT_START);
      binding.btnAnotherWay.setVisibility(View.VISIBLE);
      binding.btnAnotherWay.setText(binding.getRoot().getResources().getString(text));
      binding.btnAnotherWay.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
              dialogButtonClickLister.onClick(view, dialog);
          }
      });
      return dialogUtil;
  }

  public DialogUtil setNegativeButton(String text, DialogButtonClickLister dialogButtonClickLister) {
      binding.btnTryAgain.setVisibility(View.VISIBLE);
      binding.btnTryAgain.setText(text == null ? "" : text);
      binding.btnTryAgain.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
              dialogButtonClickLister.onClick(view, dialog);
          }
      });
      return dialogUtil;
  }

  public DialogUtil setNegativeButton(int text, DialogButtonClickLister dialogButtonClickLister) {
      binding.btnTryAgain.setVisibility(View.VISIBLE);
      binding.btnTryAgain.setText(binding.getRoot().getResources().getString(text));
      binding.btnTryAgain.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
              dialogButtonClickLister.onClick(view, dialog);
          }
      });
      return dialogUtil;
  }

  public DialogUtil setNegativeButton(String text, Drawable drawableLeft, DialogButtonClickLister dialogButtonClickLister) {
      ((MaterialButton)binding.btnAnotherWay).setIcon(drawableLeft);
      ((MaterialButton)binding.btnAnotherWay).setIconGravity(MaterialButton.ICON_GRAVITY_TEXT_START);
      binding.btnTryAgain.setVisibility(View.VISIBLE);
      binding.btnTryAgain.setText(text == null ? "" : text);
      binding.btnTryAgain.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
              dialogButtonClickLister.onClick(view, dialog);
          }
      });
      return dialogUtil;
  }

  public DialogUtil setNegativeButton(int text, Drawable drawableLeft, DialogButtonClickLister dialogButtonClickLister) {
      ((MaterialButton)binding.btnAnotherWay).setIcon(drawableLeft);
      ((MaterialButton)binding.btnAnotherWay).setIconGravity(MaterialButton.ICON_GRAVITY_TEXT_START);
      binding.btnTryAgain.setVisibility(View.VISIBLE);
      binding.btnTryAgain.setText(binding.getRoot().getResources().getString(text));
      binding.btnTryAgain.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
              dialogButtonClickLister.onClick(view, dialog);
          }
      });
      return dialogUtil;
  }

  public DialogUtil setTitle(int title) {
      binding.tvTitle.setVisibility(View.VISIBLE);
      binding.tvTitle.setText(binding.getRoot().getResources().getString(title));
      return dialogUtil;
  }

  public DialogUtil setTitle(String title) {
      binding.tvTitle.setVisibility(View.VISIBLE);
      binding.tvTitle.setText(title);
      return dialogUtil;
  }

  public DialogUtil setDescText(int descText) {
      binding.tvDescText.setVisibility(View.VISIBLE);
      binding.tvDescText.setText(binding.getRoot().getResources().getString(descText));
      return dialogUtil;
  }

  public DialogUtil setDescText(String descText) {
      binding.tvDescText.setVisibility(View.VISIBLE);
      binding.tvDescText.setText(descText);
      return dialogUtil;
  }

  public DialogUtil setDescText(String descText, boolean isHtml) {
      binding.tvDescText.setVisibility(View.VISIBLE);
      if(isHtml){
          binding.tvDescText.setText(Html.fromHtml(descText));
      }
      return dialogUtil;
  }

  public DialogUtil setDescText(SpannableString ss) {
      binding.tvDescText.setVisibility(View.VISIBLE);
      binding.tvDescText.setText(ss);
      return dialogUtil;
  }

  public DialogUtil setImage(int image) {
      binding.ivImage.setVisibility(View.VISIBLE);
      binding.ivImage.setImageResource(image);
      return dialogUtil;
  }

  public DialogUtil setImage(String url) {
      binding.ivImage.setVisibility(View.VISIBLE);
      Glide.with(binding.ivImage.getContext())
              .load(url)
              .into(binding.ivImage);
      return dialogUtil;
  }

  public DialogUtil setCancelable(boolean cancelable) {
      dialogUtil.dialog.setCancelable(cancelable);
      return dialogUtil;
  }

  public void show() {
      dialogUtil.dialog.show();
  }

  public interface DialogButtonClickLister {
      void onClick(View view, Dialog dialog);
  }
}


 

Explanation of the Code

The DialogUtil class is designed to manage the functionality of a custom dialog in a modular and reusable way. Here's a deeper dive into how it works:

  1. Singleton Pattern Implementation:
    The instance(Context context) method is central to the singleton pattern, which ensures that only a single instance of the DialogUtil class exists throughout the application's lifecycle. This pattern helps in avoiding unnecessary creation of multiple dialog instances, which can lead to memory leaks or performance degradation. Whenever you need to show the dialog, you simply call DialogUtil.instance(context). If an instance of the dialog already exists, it is reused; otherwise, a new instance is created.

  2. Binding with XML Layout:
    The class uses view binding (i.e., ContentBinding) to access the views defined in the custom XML layout (LinearLayout). This approach is safer and more efficient than using findViewById, as it provides compile-time safety and reduces boilerplate code. ContentBinding.inflate(LayoutInflater.from(context)) is used to inflate the custom layout, making all its views accessible in the Java class.

  3. Dialog Customization:
    Several methods are provided to customize the dialog dynamically:

    • setTitle(int title) and setTitle(String title): These methods are used to set the title of the dialog. You can provide either a string resource ID or a direct string value. When the title is set, it becomes visible by calling binding.tvTitle.setVisibility(View.VISIBLE).

    • setDescText(...): Multiple overloaded versions of this method allow you to set the description text in different formats — plain text, HTML, or a SpannableString. This flexibility helps in displaying text with varying styles, like bold, italic, or colored text.

    • setPositiveButton(...) and setNegativeButton(...): These methods configure the buttons in the dialog. You can set the button text, optional icons, and define their click listeners. By providing an interface DialogButtonClickLister, the dialog can handle button click events in a clean and decoupled manner. The visibility of buttons is controlled dynamically; they are only shown if configured.

  4. Setting Images and Icons:
    The dialog can display images using the setImage(int image) or setImage(String url) methods. The first variant sets an image from a drawable resource, while the second uses the Glide library to load an image from a URL. This makes the dialog versatile, capable of showing both static and dynamic images.

  5. Configuring Dialog Appearance:
    The constructor initializes the dialog with default settings: it sets the content view to the inflated layout, hides the title bar (requestWindowFeature(Window.FEATURE_NO_TITLE)), and makes the background transparent. The background is set to a ColorDrawable with a transparent color, allowing for custom design elements like rounded corners or shadows to be visible.

  6. Flexibility in Reuse and Extensibility:
    The utility class allows for easy reuse throughout the app. Wherever a dialog is needed, you just call DialogUtil.instance(context) and customize it with the available methods. You can set the title, description, buttons, and images, all tailored to the specific context or use case. Additionally, since all methods return the same DialogUtil instance, you can chain method calls for cleaner code.

  7. Dynamic Content Control:
    The methods like setPositiveButton and setNegativeButton are overloaded to accept different types of inputs, such as a string, a string resource ID, or even a drawable. This allows for dynamic control over the dialog's content and appearance. For example, you can add a button with a specific text and icon, and set a click listener that performs a custom action when the button is pressed.

  8. Controlling Dialog Behavior:
    The setCancelable(boolean cancelable) method allows you to specify whether the dialog can be canceled by the user (e.g., tapping outside the dialog). This is useful for different scenarios — you might want the dialog to be cancelable when displaying an error message, but not when showing critical information.

  9. Showing the Dialog:
    The show() method simply calls the show() method of the underlying Dialog object, making it visible to the user. This method should be called after all customizations are done, ensuring the dialog appears exactly as intended.

  10. Interface for Click Handling:
    The inner DialogButtonClickLister interface allows the developer to define custom behavior for button clicks without tightly coupling the dialog's code with the rest of the application. This makes the code modular and easier to maintain or extend.

Conclusion

By encapsulating the dialog logic in a utility class with a singleton pattern, you create a highly reusable and flexible component. You can easily modify or extend the dialog's functionality without affecting other parts of your application. Moreover, you avoid the pitfalls of maintaining external libraries and ensure compatibility with future versions of Java or Kotlin. This approach leads to cleaner, more maintainable code and a more consistent user experience across your app.