Это не очередная статья про Dagger и его возможности. Не будет ни слова про другие DI фреймворки.
Цель данной публикации — продемонстрировать подход к получению зависимостей во фрагментах, диалогах и активити.
Установка слушателя для диалога
В одном из проектов, перешедших по наследству, наткнулся на следующую реализацию диалога:
public class ExampleDialogFragment extends DialogFragment {
private Listener listener;
public interface Listener {
void onMessageEntered(String msg);
}
@Override
public void onAttach (Context context) {
super.onAttach(context);
if(context instanceOf Listener) {
listener = (Listener) context;
} else {
listener = (Listener) getParentFragment();
}
}
}
«А с каких это пор компонент должен заниматься поиском слушателя» — думал я в тот момент.
Давайте сделаем так, чтобы фрагмент не знал, кто конкретно реализует интерфейс слушателя.
Многие могут сразу предложить, например, такой вариант:
public class ExampleDialogFragment extends DialogFragment {
private Listener listener;
public interface Listener {
void onMessageEntered(String msg);
}
public static DialogFragment newInstance(Listener listener) {
ExampleDialogFragment dialogFragment = new ExampleDialogFragment();
dialogFragment.listener = listener;
return dialogFragment;
}
}
и код активити, в которую будем встраивать данный диалог:
public class ExampleActivity extends AppCompatActivity {
void showDialog() {
DialogFragment dialogFragment = ExampleDialogFragment
.newInstance(new DialogFragment.Listener() {
@Override
void onMessageEntered(String msg) {
// TODO
}
});
dialogFragment.show(getFragmentManager(), "dialog");
}
}
У данного решения есть один существенный недостаток. При изменении конфигурации (к примеру, переворот экрана) получим следующую цепочку: диалог сохранит свое состояние в Bundle
и будет уничтожен -> активити будет удалена -> новый экземпляр активити будет создан -> диалог будет создан заново на основе сохраненного в Bundle
состояния. В итоге, мы потеряем ссылку на слушателя в диалоге, так как она явно не была сохранена и восстановлена. Мы, конечно, можем вручную вызвать setListener()
в одном из колбэков жизненного цикла активити, но есть и другой вариант. Так как анонимный класс мы не можем сохранить в Bundle
, равно как и экземпляры обычных классов, нам нужно нужно соблюсти следующие условия:
- Слушатель должен реализовать интерфейс
Serializable
илиParcelable
- Передать слушателя через аргументы фрагмета при его создании
setArguments(Bundle args)
- Слушатель должен быть сохранен в методе
onSaveInstanceState(Bundle outState)
- Слушатель должен быть восстановлен в методе
Dialog onCreateDialog(Bundle savedInstanceState)
Как правило интерфейс слушателя реализуют такие компоненты Android как Activity
или Fragment
. Подобные комоненты не предназначены для сохранения в Bundle
, поэтому нам надо найти другой подход к решению. Давайте попробуем передавать не самого слушателя, а «сыщика»(Provider), который способен его найти. В этом случае, нам никто не помешает сделать его сериализуемым и сохранять в Bundle
.
Если наш «сыщик» не должен менять свое состояние в процессе взаимодействия с компонентом, то можно не переопределять метод onSaveInstanceState(Bundle outState)
и при вызове метода Dialog onCreateDialog(Bundle savedInstanceState)
восстанавливать зависимость из аргументов.
Давайте посмотрим на реализацию:
public class ExampleDialogFragment extends DialogFragment {
private static final String LISTENER_PROVIDER = "listener_provider";
private Listener listener;
public interface ListenerProvider extends Serializable {
Listener from(DialogFragment dialogFragment);
}
public interface Listener {
void onMessageEntered(String msg);
}
public static DialogFragment newInstance(ListenerProvider provider) {
ExampleDialogFragment dialogFragment = new ExampleDialogFragment();
Bundle args = new Bundle();
args.putSerializable(LISTENER_PROVIDER, provider);
dialogFragment.setArguments(args);
return dialogFragment;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Bundle args = getArguments();
if(args == null || !args.containsKey(LISTENER_PROVIDER)) {
throw new IllegalStateException("Listener provider is missing");
}
ListenerProvider listenerProvider =
(ListenerProvider) args.getSerializable(LISTENER_PROVIDER);
Listener listener = listenerProvider.from(this);
...
}
}
В таком случае код нашей активити примет вид:
public class ExampleActivity extends AppCompatActivity
implements ExampleDialogFragment.Listener {
@Override
public void onMessageEntered(String msg) {
// TODO
}
void showDialog() {
DialogFragment dialogFragment = ExampleDialogFragment
.newInstance(new ListenerProvider());
dialogFragment.show(getFragmentManager(), "dialog");
}
private static class ListenerProvider
implements ExampleDialogFragment.ListenerProvider {
private static final long serialVersionUID = -5986444973089471288L;
@Override
public ExampleDialogFragment.Listener from(DialogFragment dialogFragment) {
return (ExampleDialogFragment.Listener) dialogFragment.getActivity();
}
}
}
Если нам понадобится реализовать показ диалога из фрагмента, то получим слудеющий код:
public class ExampleFragment extends Fragment
implements ExampleDialogFragment.Listener {
@Override
public void onMessageEntered(String msg) {
// TODO
}
void showDialog() {
DialogFragment dialogFragment = ExampleDialogFragment.newInstance(new ListenerProvider());
dialogFragment.show(getFragmentManager(), "dialog");
}
private static class ListenerProvider implements ExampleDialogFragment.ListenerProvider {
private static final long serialVersionUID = -5986444973089471288L;
@Override
public ExampleDialogFragment.Listener from(DialogFragment dialogFragment) {
return (ExampleDialogFragment.Listener) dialogFragment.getParentFragment();
}
}
}
В итоге мы получаем, что вызывающий компонент сам помогает диалогу найти слушателя. При этом фрагмент понятия не имеет кто именно им (слушателем) является. К примеру, если по какой-либо причине нам не хочется обращаться к Activity
напрямую, то никто нам не мешает просто кинуть событие, а затем в нужном месте поймать его и обработать (код диалога даже не потребуется менять):
public class ExampleFragment extends Fragment {
void onMessageEvent(Message message) {
// TODO
}
void showDialog() {
DialogFragment dialogFragment = ExampleDialogFragment.newInstance(new ListenerProvider());
dialogFragment.show(getFragmentManager(), "dialog");
}
private static class Message {
public final String content;
private Message(String content) {
this.content = content;
}
}
private static class ListenerProvider implements ExampleDialogFragment.ListenerProvider {
private static final long serialVersionUID = -5986444973089471288L;
@Override
public ExampleDialogFragment.Listener from(DialogFragment dialogFragment) {
return new ExampleDialogFragment.Listener() {
@Override
public void onMessageEntered(String msg) {
EventBus.getDefault().post(new Message(msg));
}
};
}
}
}
С диалогами вроде разобрались. Едем дальше.
Поиск зависимостей во фрагментах
Часто приходится организовывать взаимодействие типа Activity <-> Fragment
или Fragment <-> Fragment
в рамках одной активити. Общий принцип остается тот же, что был описан выше: посредством интерфейса (к примеру, Listener) и «сыщика» организуется общение между компонентами. В рамках данной статьи будем рассматривать одностороннее взаимодействие.
В качестве примера, разберем случай с получением презентера во фрагменте. Думаю, каждый из нас сталкивался с подобным:
public interface Presenter {
...
}
public class ExampleFragment extends Fragment {
private Presenter presenter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
presenter = App.get().getExampleFragmentComponent().getPresenter();
}
}
И все вроде хорошо, создание зависимостей мы спрятали, а вот подобное их получение теперь разбросано по всему проекту. На мой взгляд, как минимум тот, кто вызывает, должен либо предоставить эти зависимости, либо помочь их найти.
Опять применим наш прием с «сыщиком»:
public class ExampleFragment extends Fragment {
private static final String DI_PROVIDER = "di_provider";
private Presenter presenter;
public interface DependencyProvider implements Serializable {
Presenter getPresenterOf(Fragment fragment);
}
public static Fragment newInstance(DependencyProvider dependencyProvider) {
Fragment fragment = new ExampleFragment();
Bundle args = new Bundle();
args.putSerializable(DI_PROVIDER, dependencyProvider);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
if(args == null || !args.containsKey(DI_PROVIDER)) {
throw new IllegalStateException("DI provider is missing");
}
DependencyProvider diProvider =
(DependencyProvider) args.getSerializable(DI_PROVIDER);
presenter = diProvider.getPresenterOf(this);
}
}
public class ExampleActivity extends AppCompatActivity {
void showFragment() {
FragmentTransaction ft = getFragmentManager().beginTransaction();
Fragment fragment = ExampleFragment
.newInstance(new DiProvider());
ft.add(R.id.container, fragment);
ft.commit();
}
private static class DiProvider
implements ExampleFragment.DependencyProvider {
private static final long serialVersionUID = -5986444973089471288L;
@Override
public Presenter get(Fragment fragment) {
return App.get().getExampleFragmentComponent().getPresenter();
}
}
}
Таким образом, мы сделали наш фрагмент более универсальным, соответственно, можем без проблем переносить его из проекта в проект не меняя код компонента без надобности.
Аналогичным способом можно организовать получение зависимостей в Activity
.
Небольшой пример с реализацией подобного подхода лежит здесь.
Надеюсь, описанный подход пригодится Вам в реализации проектов и принесет пользу.
Спасибо за внимание!
Автор: Константин