example - arquitectura de componentes android
Compartir datos entre fragmentos utilizando el nuevo componente de arquitectura ViewModel (6)
Antes de utilizar una devolución de llamada que se adjunta a la Actividad que se considera como un contenedor.
Esa devolución de llamada es un intermediario entre dos Fragmentos.
Las cosas malas de esta solución anterior son:
- Activity tiene que llevar la devolución de llamada, significa mucho trabajo para Activity.
- Dos fragmentos están estrechamente acoplados, es difícil actualizar o cambiar la lógica más tarde.
Con el nuevo ViewModel (con soporte de LiveData), tiene una solución elegante. Ahora desempeña un papel de intermediario al que puede adjuntar su ciclo de vida a la Actividad.
- La lógica y los datos entre dos fragmentos ahora se encuentran en ViewModel.
- Two Fragment obtiene datos / estado de ViewModel, por lo que no necesitan conocerse entre sí.
- Además, con el poder de LiveData, puede cambiar el Fragmento de detalle en función de los cambios del Fragmento maestro en un enfoque reactivo en lugar de la forma de devolución de llamada anterior.
Ahora se deshace por completo de la devolución de llamada que se une estrechamente tanto a la actividad como a los fragmentos relacionados.
Le recomiendo a través
del laboratorio de códigos de Google
.
En el paso 5, puede encontrar un buen ejemplo sobre esto.
En Last Google IO, Google lanzó una vista previa de algunos componentes nuevos del arco, uno de los cuales, ViewModel.
En los docs google muestra uno de los posibles usos de este componente:
Es muy común que dos o más fragmentos de una actividad necesiten comunicarse entre sí. Esto nunca es trivial, ya que ambos fragmentos necesitan definir alguna descripción de la interfaz, y la actividad del propietario debe unir los dos. Además, ambos fragmentos deben manejar el caso donde el otro fragmento aún no se ha creado o no es visible.
Este punto de dolor común puede abordarse mediante el uso de objetos ViewModel. Imagine un caso común de fragmentos de detalles maestros, donde tenemos un fragmento en el que el usuario selecciona un elemento de una lista y otro fragmento que muestra el contenido del elemento seleccionado.
Estos fragmentos pueden compartir un ViewModel utilizando su ámbito de actividad para manejar esta comunicación.
Y muestra un ejemplo de implementación:
public class SharedViewModel extends ViewModel {
private final MutableLiveData<Item> selected = new MutableLiveData<Item>();
public void select(Item item) {
selected.setValue(item);
}
public LiveData<Item> getSelected() {
return selected;
}
}
public class MasterFragment extends Fragment {
private SharedViewModel model;
public void onActivityCreated() {
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {
model.select(item);
});
}
}
public class DetailFragment extends LifecycleFragment {
public void onActivityCreated() {
SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
model.getSelected().observe(this, { item ->
// update UI
});
}
}
Estaba bastante entusiasmado con la posibilidad de no necesitar esas interfaces utilizadas para que los fragmentos se comuniquen a través de la actividad.
Pero el ejemplo de Google no muestra exactamente cómo llamaría al fragmento de detalle de master.
Todavía tendría que usar una interfaz que será implementada por la actividad, que llamará fragmentManager.replace (...), ¿o hay otra forma de hacerlo usando la nueva arquitectura?
He encontrado una solución similar a otras según el example Google Codelabs. Tengo dos fragmentos donde uno de ellos espera que un objeto cambie en el otro y continúa su proceso con un objeto actualizado.
para este enfoque, necesitará una clase ViewModel como se muestra a continuación:
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.ViewModel;
import yourPackage.YourObjectModel;
public class SharedViewModel extends ViewModel {
public MutableLiveData<YourObjectModel> item = new MutableLiveData<>();
public YourObjectModel getItem() {
return item.getValue();
}
public void setItem(YourObjectModel item) {
this.item.setValue(item);
}
}
y el fragmento del oyente debería verse así:
public class ListenerFragment extends Fragment{
private SharedViewModel model;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
model.item.observe(getActivity(), new Observer<YourObjectModel>(){
@Override
public void onChanged(@Nullable YourObjectModel updatedObject) {
Log.i(TAG, "onChanged: recieved freshObject");
if (updatedObject != null) {
// Do what you want with your updated object here.
}
}
});
}
}
Finalmente, el fragmento actualizador puede ser así:
public class UpdaterFragment extends DialogFragment{
private SharedViewModel model;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
}
// Call this method where it is necessary
private void updateViewModel(YourObjectModel yourItem){
model.setItem(yourItem);
}
}
Es bueno mencionar que el fragmento actualizador puede ser cualquier forma de fragmento (no solo DialogFragment) y para usar estos componentes de arquitectura debe tener estas líneas de códigos en el archivo build.gradle de su aplicación. source
dependencies {
def lifecycle_version = "1.1.1"
implementation "android.arch.lifecycle:extensions:$lifecycle_version"
}
Implementé algo similar a lo que desea, mi modelo de vista contiene el objeto LiveData que contiene el estado Enum, y cuando desea cambiar el fragmento de maestro a detalles (o al revés), llama a las funciones de ViewModel que cambian el valor de livedata y la actividad sabe cambia el fragmento porque está observando un objeto livedata.
TestViewModel:
public class TestViewModel extends ViewModel {
private MutableLiveData<Enums.state> mState;
public TestViewModel() {
mState=new MutableLiveData<>();
mState.setValue(Enums.state.Master);
}
public void onDetail() {
mState.setValue(Enums.state.Detail);
}
public void onMaster() {
mState.setValue(Enums.state.Master);
}
public LiveData<Enums.state> getState() {
return mState;
}
}
Enumeraciones:
public class Enums {
public enum state {
Master,
Detail
}
}
TestActivity:
public class TestActivity extends LifecycleActivity {
private ActivityTestBinding mBinding;
private TestViewModel mViewModel;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding=DataBindingUtil.setContentView(this, R.layout.activity_test);
mViewModel=ViewModelProviders.of(this).get(TestViewModel.class);
mViewModel.getState().observe(this, new Observer<Enums.state>() {
@Override
public void onChanged(@Nullable Enums.state state) {
switch(state) {
case Master:
setMasterFragment();
break;
case Detail:
setDetailFragment();
break;
}
}
});
}
private void setMasterFragment() {
MasterFragment masterFragment=MasterFragment.newInstance();
getSupportFragmentManager().beginTransaction().replace(R.id.frame_layout, masterFragment,"MasterTag").commit();
}
private void setDetailFragment() {
DetailFragment detailFragment=DetailFragment.newInstance();
getSupportFragmentManager().beginTransaction().replace(R.id.frame_layout, detailFragment,"DetailTag").commit();
}
@Override
public void onBackPressed() {
switch(mViewModel.getState().getValue()) {
case Master:
super.onBackPressed();
break;
case Detail:
mViewModel.onMaster();
break;
}
}
}
MasterFragment:
public class MasterFragment extends Fragment {
private FragmentMasterBinding mBinding;
public static MasterFragment newInstance() {
MasterFragment fragment=new MasterFragment();
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mBinding=DataBindingUtil.inflate(inflater,R.layout.fragment_master, container, false);
mBinding.btnDetail.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final TestViewModel viewModel=ViewModelProviders.of(getActivity()).get(TestViewModel.class);
viewModel.onDetail();
}
});
return mBinding.getRoot();
}
}
Detalle Fragmento:
public class DetailFragment extends Fragment {
private FragmentDetailBinding mBinding;
public static DetailFragment newInstance() {
DetailFragment fragment=new DetailFragment();
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mBinding=DataBindingUtil.inflate(inflater,R.layout.fragment_detail, container, false);
mBinding.btnMaster.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final TestViewModel viewModel=ViewModelProviders.of(getActivity()).get(TestViewModel.class);
viewModel.onMaster();
}
});
return mBinding.getRoot();
}
}
Puede establecer valores de Fragmento de detalle a Fragmento maestro como este
model.selected.setValue(item)
Termino usando el propio ViewModel para detener al oyente que activará el método Activity. Similar a la antigua pero, como dije, pasando el oyente a ViewModel en lugar del fragmento. Entonces mi ViewModel se veía así:
public class SharedViewModel<T> extends ViewModel {
private final MutableLiveData<T> selected = new MutableLiveData<>();
private OnSelectListener<T> listener = item -> {};
public interface OnSelectListener <T> {
void selected (T item);
}
public void setListener(OnSelectListener<T> listener) {
this.listener = listener;
}
public void select(T item) {
selected.setValue(item);
listener.selected(item);
}
public LiveData<T> getSelected() {
return selected;
}
}
en StepMasterActivity obtengo el ViewModel y lo configuro como un oyente:
StepMasterActivity.class:
SharedViewModel stepViewModel = ViewModelProviders.of(this).get("step", SharedViewModel.class);
stepViewModel.setListener(this);
...
@Override
public void selected(Step item) {
Log.d(TAG, "selected: "+item);
}
...
En el fragmento acabo de recuperar el ViewModel
stepViewModel = ViewModelProviders.of(getActivity()).get("step", SharedViewModel.class);
y llama:
stepViewModel.select(step);
Lo probé superficialmente y funcionó. A medida que vaya implementando las otras funciones relacionadas con esto, estaré al tanto de cualquier problema que pueda ocurrir.
Actualizado el 12/06/2017,
Android Official proporciona un ejemplo simple y preciso para ilustrar cómo funciona ViewModel en la plantilla Master-Detail, primero debe echarle un vistazo. Compartir datos entre fragmentos
Como @CommonWare, @Quang Nguyen mencionó, no es el propósito de Yigit hacer la llamada del maestro al detalle, sino ser mejor usar el patrón del intermediario. Pero si desea realizar alguna transacción fragmentaria, debe hacerse en la actividad. En ese momento, la clase ViewModel debe ser como clase estática en Activity y puede contener alguna Devolución de llamada fea para devolver la actividad y realizar la transacción del fragmento.
He intentado implementar esto y hacer un proyecto simple sobre esto. Puedes echarle un vistazo. La mayor parte del código está referenciado desde Google IO 2017, también la estructura. https://github.com/charlesng/SampleAppArch
No uso Master Detail Fragment para implementar el componente, sino el anterior (comunicación entre fragmentos en ViewPager). La lógica debe ser la misma.
Pero descubrí que algo es importante usando estos componentes
- Lo que desea enviar y recibir en el intermediario, deben enviarse y recibirse solo en View Model
- La modificación no parece demasiado en la clase de fragmento. Dado que solo cambia la implementación de "Devolución de llamada de interfaz" a "Escuchar y responder ViewModel"
- View Model initialize parece importante y es probable que se llame en la actividad.
- Usar MutableLiveData para sincronizar la fuente solo en actividad.
Actividad 1.Pager
public class PagerActivity extends LifecycleActivity {
/**
* The pager widget, which handles animation and allows swiping horizontally to access previous
* and next wizard steps.
*/
private ViewPager mPager;
private PagerAgentViewModel pagerAgentViewModel;
/**
* The pager adapter, which provides the pages to the view pager widget.
*/
private PagerAdapter mPagerAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pager);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
mPager = (ViewPager) findViewById(R.id.pager);
mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
mPager.setAdapter(mPagerAdapter);
pagerAgentViewModel = ViewModelProviders.of(this).get(PagerAgentViewModel.class);
pagerAgentViewModel.init();
}
/**
* A simple pager adapter that represents 5 ScreenSlidePageFragment objects, in
* sequence.
*/
private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
...Pager Implementation
}
}
2.PagerAgentViewModel (merecía un nombre mejor en lugar de este)
public class PagerAgentViewModel extends ViewModel {
private MutableLiveData<String> messageContainerA;
private MutableLiveData<String> messageContainerB;
public void init()
{
messageContainerA = new MutableLiveData<>();
messageContainerA.setValue("Default Message");
messageContainerB = new MutableLiveData<>();
messageContainerB.setValue("Default Message");
}
public void sendMessageToB(String msg)
{
messageContainerB.setValue(msg);
}
public void sendMessageToA(String msg)
{
messageContainerA.setValue(msg);
}
public LiveData<String> getMessageContainerA() {
return messageContainerA;
}
public LiveData<String> getMessageContainerB() {
return messageContainerB;
}
}
3.BlankFragmentA
public class BlankFragmentA extends LifecycleFragment {
public BlankFragmentA() {
// Required empty public constructor
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
//setup the listener for the fragment A
ViewModelProviders.of(getActivity()).get(PagerAgentViewModel.class).getMessageContainerA().observe(this, new Observer<String>() {
@Override
public void onChanged(@Nullable String msg) {
textView.setText(msg);
}
});
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_blank_a, container, false);
textView = (TextView) view.findViewById(R.id.fragment_textA);
// set the onclick listener
Button button = (Button) view.findViewById(R.id.btnA);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ViewModelProviders.of(getActivity()).get(PagerAgentViewModel.class).sendMessageToB("Hello B");
}
});
return view;
}
}
4.BlankFragmentB
public class BlankFragmentB extends LifecycleFragment {
public BlankFragmentB() {
// Required empty public constructor
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
//setup the listener for the fragment B
ViewModelProviders.of(getActivity()).get(PagerAgentViewModel.class).getMessageContainerB().observe(this, new Observer<String>() {
@Override
public void onChanged(@Nullable String msg) {
textView.setText(msg);
}
});
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_blank_b, container, false);
textView = (TextView) view.findViewById(R.id.fragment_textB);
//set the on click listener
Button button = (Button) view.findViewById(R.id.btnB);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ViewModelProviders.of(getActivity()).get(PagerAgentViewModel.class).sendMessageToA("Hello A");
}
});
return view;
}
}