الگوی Event Handling و CallBack در اندروید

یکی از الگوهای معروف و ساده ای که میشه با اینترفیس ها پیاده سازی کرد همین Event Handling هست. و احتمالا جاهای مختلفی هم از آنها استفاده کردید. به خصوص وقتی دو تا کلاس داریم که ارتباط تنگاتنگی با هم دارند.

درو واقع الگوهای "listener" و "observer" از رایج ترین استراتژی ها برای ایجاد callback (صدا کردن یا مشاهده کردن نتیجه ای از یک قطعه کد) به صورت موازی در برنامه نویسی اندروید هستند.

یکی از اینترفیس هایی که به همین منظور استفاده می شود همین setOn ClickListener معروف هست:

https://gist.github.com/imansdn/14d3d54a1338b3f71a0be4741cdf1121



ما اینطوری میتونیم پیچیدگی های کد رو با مجزا سازی قسمتهای مختلف کد در قالب کلاس های مختلف کم کنیم (پیروی از قانون کپسوله سازی) و در جای درست و منطقی به هر قسمتی هم که بخواهیم دسترسی داشته باشیم.


مراحل ساخت یک listener سفارشی در قالب یک مثال

یک کلاس داریم (کلاس child) که یه سری عملیات برای ریکوئست به سرور رو داریم اونجا انجام میدیم.
و میخواهیم فقط نتیجه ی این عملیات رو به محض اینکه انجام شد به یک کلاس دیگه (کلاس parent) خبر بدیم.
یک اینترفیس به نام NetworkRequestListener درون کلاس child می سازیم که درون آن یک متد به نام onResult می نویسیم.

public class ChildClass {
    public interface NetworkRequestListener {
      public void onResult(SomeData data); // این متد میتواند ورودی هم نداشته باشد
      }
  }

یک نمونه از اینترفیس NetworkRequestListener را در همین کلاس child (کلاسی که عملیات انجام می شود) می سازیم:

public class ChildClass {
private NetworkRequestListener listener;
    public interface NetworkRequestListener {
     public void onResult(SomeData data); // این متد میتواند ورودی هم نداشته باشد
        }
  }

کار دیگه ای که باید اینجا انجام بشه اینه که توی سازنده ی کلاس child این instant ای که به نام listener ساختیم را null کنیم .چرا؟ چون قصد داریم با یک متد setter این listenr را مقدار بدهیم و از آنجایی که امکان دارد که این setter از جاهای مختلفی صدا زده شود ، هر بار در سازنده مقدارش را به null بر میگردانیم.

public class ChildClass { 
private NetworkRequestListener listener;   
 public ChildClass() {
    this.listener = null; 
 }
 public interface NetworkRequestListener {    
  public void onResult(SomeData data); // این متد میتواند ورودی هم نداشته باشد    
       } 
  }

حالا این setter ای که ازش صحبت کردیم هم به این صورت تعریف میکنیم:

public class ChildClass {  
 private NetworkRequestListener listener;  
 public ChildClass() {  
    this.listener = null;  
     } 
       public void setResultListener(NetworkRequestListener listener) {
        this.listener = listener;
        }

   public interface NetworkRequestListener {  
   public void onResult(SomeData data); // این متد میتواند ورودی هم نداشته باشد
       }  
     }

حالا در این کلاس یک متد برای ارتباط با سرور داریم که loadData نام دارد و ما باید متد result از متغیرlistener ای که ساختیم را درست جایی که result را می گیریم صدا بزنیم:

 public void loadData() {
      AsyncHttpClient client = new AsyncHttpClient();
      client.get("https://mycustomapi.com/data/get.json", new JsonHttpResponseHandler() {         
         @Override
          public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
                          SomeData data = SomeData.processData(response.get("data"));
               if (listener != null)
                listener.onSuccess(data); 
                }
                    });
     }

توی این کلاس دیگه کاری نداریم ، حالا بگذارید کلاس parrent را بررسی کنیم .
کارهایی که باید در کلاس parent انجام بدیم :
اولا متد setter رو بتونیم صدا بزنیم.
دوما از متدهای پیاده سازی شده استفاده کنیم!

public class MyParentActivity extends Activity {
  @Override
     protected void onCreate(Bundle savedInstanceState) {
     ChildClass child = new ChildClass();
    //اینجا کلاس ستر رو صدا میزنیم 
    child.setResultListener ( new  ChildClass.NetworkRequestListener() {
      @Override
        public void onResult(SomeData data) {
        	 
        }
        });
         }
         }


خب ما اینجا اینترفیس listener مان را از طریق متد setter مقدار دهی کردیم . حقیقت اینه که همیشه حتما نیاید یک متد setter بنویسیم ، دو راه دیگر هم وجود دارد برای این کار :
مقدار دهی از طریق سازنده :
برای این کار باید در کلاس child به این صورت عمل می کردیم:

private ChildClass listener;
public ChildClass(NetworkRequestListener listener) {
 this.listener = listener; 
    }

و در کلاس parent به این صورت داشتیم:

ChildClass child = new ChildClass ( new ChildClass.NetworkRequestListener() {
  @Override
    public void onResult(SomeData data) {
    //we have result here!
      });
      }


مقدار دهی از طریق lifeCycle event ها :

این مورد که بیشتر در استفاده از Fragment ها مرسوم است . به این صورت است که ما در اکتیویتی به جای استفاده از setter و یا سازنده برای مقدار دهی listener کلاس child ( که اینجا همان fragment است) از یکی از متدهای چرخه ی حیات اندروید به نام onAttach استفاده میکنیم و از آن می پرسیم که آیا fragment ای به این اکتیویتی attach شده است یا خیر و در صورتی که فرگمنت یافت شد ، آن وقت listener درون آن را مقدار دهی میکنیم.

کمی پایین تر یکی از مثال ها ی گوگل را برای ارتباط بین fragment و اکتیویتی بررسی میکنیم که آنجا از onAttach استفاده شده است.


استفاده های مرسوم و معروف

مثلا بعضی ها چون میخواهند در اکتیویتی ای که recyclerView رو دارند همانجا یک clickListener برای هر آیتم لیست بنویسند و نه در adapter ، به خصوص زمانی که محاسباتی در هنگام کلیک روی هر آیتم نیاز است انجام شود که جای آن در adapter نیست. می شود یک اینترفیس تعریف کرد و event کلیک شدن رو در اکتیویتی هندل کرد.

یا وقتی از FragmentDialog استفاده میشه و Dialoge یک کلاس جدا هست و میخواهیم توی اکتیویتی که دیالوگ باز میشه بگیم وقتی روی هر قسمت دیالوگ کلیک شد یک کار مشخص انجام بده که احتمالا اون کار وابسته به اکتیویتی باشد. میتوانیم برای رخداد هایی که میخواهیم اینترفیس callback بسازیم و تک تک آنها را درون اکتیویتی هندل کنیم.

یا وقتی یک Fragment از قسمت منوی file --> new -->fragment میسازید میبینید که خود اندروید استودیو یک اینترفیس callback به صورت پیش فرض می سازد و از ما میخواهد که آن را در اکتیویتی میزبان اون فرگمنت پیاده سازی کنیم . در واقع میخواهد به ما یاد بدهد که برای ارتباط بین فرگمنت و اکتیویتی میزبان و یا همچنین برای ارتباط بین دو فرگمنت ، میتونیم از این الگو استفاده کنیم.
اگر چه که اگر از معماری MVVM استفاده کنید و یک viewModel را بین هر دو فرگمنت Share کنید راه بهتری برای این ارتباط است.

این مثالی است که گوگل برای ارتباط بین Fragment و اکتیویتی میزبان داده است ، بیایید با هم بررسی اش کنیم:

HeadlinesFragment

public class HeadlinesFragment extends ListFragment {
OnHeadlineSelectedListener callback;

public void setOnHeadlineSelectedListener(OnHeadlineSelectedListener callback) {
this.callback = callback;
}

public interface OnHeadlineSelectedListener {
public void onArticleSelected(int position);
}

@Override
public void onListItemClick(ListView l, View v, int position, long id) {
// Send the event to the host activity
callback.onArticleSelected(position);
}

// ...
}


MainActivity

public static class MainActivity extends Activity implements HeadlinesFragment.OnHeadlineSelectedListener{
    // ...

    @Override
    public void onAttachFragment(Fragment fragment) {
        if (fragment instanceof HeadlinesFragment) {
            HeadlinesFragment headlinesFragment = (HeadlinesFragment) fragment;
            headlinesFragment.setOnHeadlineSelectedListener(this);
        }
    }

  public void onArticleSelected(int position) {
            ArticleFragment newFragment = new ArticleFragment();
            Bundle args = new Bundle();
            args.putInt(ArticleFragment.ARG_POSITION, position);
            newFragment.setArguments(args);

            FragmentTransaction transaction =             
            getSupportFragmentManager().beginTransaction();

            transaction.replace(R.id.fragment_container, newFragment);
            transaction.addToBackStack(null);

            transaction.commit();
    }
}

یکی دیگه از مواردی که از این الگو استفاده میکنیم و خیلی هم مفیده و به درد میخورد زمانی است که یک callback برای یک کلاس asynchronous می نویسیم و نتیجه ی error یا success رو فقط ازش میگیریم.مثلا فکر کنید یک کلاس مجزا ساختید برای ارتباط با سرور و اونجا یک نمونه از اینترفیس تون میسازید و در جای درست onSuccess و رو صدا میزنید. اینطوری بیرون از آن کلاس میتونید به اون لحظه که عملیات موفق آمیز بوده و یا با شکست رو به رو شده دسترسی داشته باشید و اکشن مناسبی رو نشون بدید.


نام گذاری ها

نام گذاری اینترفیس callback معمولا به صورت PascalCase هستند و با On یا SetOn شروع می شود سپس یک اسم مرتبط با آن افزوده می شود و در نهایت با یک Listener پایان می یابد :

On + {select} + Listener
SetOn + {select} + Listener

نام گذاری متد های درون اینترفیس هم طبق معمول camelCase هستند ، با on به علاوه ی یک نام مرتبط و کوتاه که وظیفه ی آن متد را با یک نگاه برساند نوشته می شوند:

on+{Edit}
on+{Delete}
on+{Exit}


منابع

https://guides.codepath.com/android/Creating-Custom-Listeners

https://developer.android.com/training/basics/fragments/communicating.html