This project is an extension of the Master/Detail flow template that can be found in Android Studio. The difference is that this application uses a single Activity and a ViewPager that manages three Fragments. The third Fragment is the Master (list) Fragment, which contains a clickable RecycleView. When a list item is clicked, it switches the Fragment with the Child (detail) Fragment.
While the project works, I'd like to avoid using android:configChanges= "orientation|keyboardHidden|screenSize"
in the manifest. How should I do this?
If the attribute is removed, mListener in ItemListFragment is destroyed along with ItemFragmentList when rotating but is never re-created when ItemListFragment is re-created. This results in nothing happening when clicking on a List item in Portrait mode.
My initial solution was to manually override the configuration change handling, which meant ItemListFragment was not destroyed when rotating the screen. onConfigurationChanged()
and populateViewForOrientation()
were added to re-inflate the layout. Surely there is a better solution than to manually override configuration handling.
Project available at: https://github.com/lukeallison/ViewPagerMasterDetail, Video: http://tinypic.com/r/1zltyeq/9
BaseFragment.java
public class BaseFragment extends Fragment {
public Bridge mBridget;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mBridget = (MainActivity) getActivity();
}
}
Interface: Bridge.java
public interface Bridge {
abstract void onBack();
}
ItemListFragment.java
public class ItemListFragment extends BaseFragment{
private boolean mTwoPane = false;
private PageFragmentListener mListener;
public static ItemListFragment newInstance(PageFragmentListener listener) {
ItemListFragment fragment = new ItemListFragment();
fragment.mListener = listener;
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_item_list, container, false);
initLayout(root);
return root;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
LayoutInflater inflater = LayoutInflater.from(getActivity());
populateViewForOrientation(inflater, (ViewGroup) getView());
}
private void populateViewForOrientation(LayoutInflater inflater, ViewGroup viewGroup) {
viewGroup.removeAllViewsInLayout();
View subview = inflater.inflate(R.layout.fragment_item_list, viewGroup);
initLayout(subview);
}
public void initLayout(View root) {
View recyclerView = root.findViewById(R.id.item_list);
mTwoPane = false;
if (root.findViewById(R.id.item_detail_container) != null) { // R.layout.list_item is located "layout", "layout-land"..
mTwoPane = true; // currently loaded "layout-land/list_item". landscape mode
}
Toolbar toolbar = (Toolbar) root.findViewById(R.id.toolbar);
((AppCompatActivity)getActivity()).setSupportActionBar(toolbar);
toolbar.setTitle("List");
assert recyclerView != null;
setupRecyclerView((RecyclerView) recyclerView);
}
private void setupRecyclerView(RecyclerView recyclerView) {
recyclerView.setAdapter(new SimpleItemRecyclerViewAdapter(DummyContent.ITEMS));
}
public class SimpleItemRecyclerViewAdapter
extends RecyclerView.Adapter<SimpleItemRecyclerViewAdapter.ViewHolder> {
private final List<DummyContent.DummyItem> mValues;
public SimpleItemRecyclerViewAdapter(List<DummyContent.DummyItem> items) {
mValues = items;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_list_content, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
holder.mItem = mValues.get(position);
holder.mIdView.setText(mValues.get(position).id);
holder.mContentView.setText(mValues.get(position).content);
// One row of List. define click event
holder.mView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mTwoPane) { // landscape mode
Bundle arguments = new Bundle();
arguments.putString(Constants.ARG_ITEM_ID, holder.mItem.id);
ItemTwoDetailFragment fragment = ItemTwoDetailFragment.newInstance();
fragment.setArguments(arguments);
// show detail fragment to right side of screen
getActivity().getSupportFragmentManager().beginTransaction()
.replace(R.id.item_detail_container, fragment)
.commit();
} else { // portrait mode
if (mListener!=null)
mListener.onSwitchToNextFragment(holder.mItem.id); // switch detail fragment
}
}
});
}
@Override
public int getItemCount() {
return mValues.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
public final View mView;
public final TextView mIdView;
public final TextView mContentView;
public DummyContent.DummyItem mItem;
public ViewHolder(View view) {
super(view);
mView = view;
mIdView = (TextView) view.findViewById(R.id.id);
mContentView = (TextView) view.findViewById(R.id.content);
}
@Override
public String toString() {
return super.toString() + " '" + mContentView.getText() + "'";
}
}
}
}
ItemOneDetailFragment.java
/**
* A fragment representing a single Item detail screen.
* on handsets.
*/
public class ItemOneDetailFragment extends BaseFragment {
private DummyContent.DummyItem mItem;
/*
Listener for switch fragment
*/
private PageFragmentListener mListener;
public static ItemOneDetailFragment newInstance(PageFragmentListener listener) {
ItemOneDetailFragment fragment = new ItemOneDetailFragment();
fragment.mListener = listener;
return fragment;
}
public ItemOneDetailFragment() {
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments().containsKey(Constants.ARG_ITEM_ID)) {
mItem = DummyContent.ITEM_MAP.get(getArguments().getString(Constants.ARG_ITEM_ID)); // Get selected Item ID to show details
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_item_one_detail, container, false);
// Show the dummy content as text in a TextView.
if (mItem != null) {
Toolbar toolbar = (Toolbar) rootView.findViewById(R.id.detail_toolbar);
toolbar.setTitle(mItem.content);
toolbar.setNavigationIcon(R.drawable.ic_ab_back_material);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mBridget.onBack();
}
});
((TextView) rootView.findViewById(R.id.item_detail)).setText(mItem.details); // show details
}
return rootView;
}
}
ItemTwoDetailFragment.java
// for landscape orientation
public class ItemTwoDetailFragment extends BaseFragment {
private DummyContent.DummyItem mItem;
public static ItemTwoDetailFragment newInstance() {
return new ItemTwoDetailFragment();
}
public ItemTwoDetailFragment() {
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments().containsKey(Constants.ARG_ITEM_ID)) {
mItem = DummyContent.ITEM_MAP.get(getArguments().getString(Constants.ARG_ITEM_ID)); // Get selected item to show details
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_item_two_detail, container, false);
// Show the dummy content as text in a TextView.
if (mItem != null) {
((TextView) rootView.findViewById(R.id.item_detail)).setText(mItem.details); // show details
}
return rootView;
}
}
MainActivity.java
public class MainActivity extends AppCompatActivity implements Bridge{
private ViewPager viewPager = null;
private MyAdapter mAdapter;
PageChangeListener mListener = new PageChangeListener();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewPager = (ViewPager) findViewById(R.id.pager);
viewPager.setOnPageChangeListener(mListener); // Page Change Listener
mAdapter = new MyAdapter(getSupportFragmentManager());
viewPager.setAdapter(mAdapter);
// Show the Up button in the action bar.
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
@Override
public void onBackPressed() {
if (mListener.getCurrentPage()==2 && mAdapter.mFragment instanceof ItemOneDetailFragment) { // current page is Tab-3, current fragment is detail fragment
mAdapter.mListener.onSwitchToNextFragment("0"); // show List fragment
return; // prevent to finish app.
}
super.onBackPressed();
}
// Do the same thing as the back button - go back to ItemListFragment
// Only when in ItemOneDetailFragment
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
if (mListener.getCurrentPage() == 2 && mAdapter.mFragment instanceof ItemOneDetailFragment) { // current page is Tab-3, current fragment is detail fragment
mAdapter.mListener.onSwitchToNextFragment("0"); // show List fragment
// return; // prevent to finish app.
}
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBack() {
onBackPressed();
}
}
class PageChangeListener extends ViewPager.SimpleOnPageChangeListener {
private int currentPage;
@Override
public void onPageSelected(int position) {
currentPage = position; // current selected page
}
public final int getCurrentPage() {
return currentPage;
}
}
/**
* Fragment Page Adapter
*/
class MyAdapter extends FragmentPagerAdapter{
private final FragmentManager mFragmentManager;
public BaseFragment mFragment;
/**
* PageFragmentListener for switching fragment.
*/
public PageFragmentListener mListener = new PageFragmentListener() {
@Override
public void onSwitchToNextFragment(final String id) {
mFragmentManager.beginTransaction().remove(mFragment).commit();
if (mFragment instanceof ItemListFragment){ // current fragment is List Fragment
Bundle arguments = new Bundle();
arguments.putString(Constants.ARG_ITEM_ID, id); // selected item id
mFragment = ItemOneDetailFragment.newInstance(mListener); // switch detail fragment
mFragment.setArguments(arguments);
}else{ // DetailFragment
mFragment = ItemListFragment.newInstance(mListener); // => switch list fragment
}
notifyDataSetChanged(); // notify changes
}
};
public MyAdapter(FragmentManager fm) {
super(fm);
mFragmentManager = fm;
}
@Override
public Fragment getItem(int position) {
if (position == 0) // Tab-1
return FragmentA.newInstance();
if (position == 1) // Tab-2
return FragmentB.newInstance();
if (position == 2) { // Tab-3
if (mFragment==null) // first time => create list fragment
mFragment = ItemListFragment.newInstance(mListener);
return mFragment;
}
return null;
}
@Override
public CharSequence getPageTitle(int position) {
if (position == 0) { // Tab-1
return "Tab 1";
}
if (position == 1) { // Tab-2
return "Tab 2";
}
if (position == 2) { //Tab-3
return "Tab 3";
}
return null;
}
@Override
public int getCount() { // Count of Tabs
return 3;
}
@Override
public int getItemPosition(Object object) {
Log.i("Adapter", "ItemPosition>>>" + object.toString());
if (object instanceof ItemListFragment && mFragment instanceof ItemOneDetailFragment) { // fragment changed
return POSITION_NONE;
}
if (object instanceof ItemOneDetailFragment && mFragment instanceof ItemListFragment) { // fragment changed
return POSITION_NONE;
}
return POSITION_UNCHANGED;
}
}
Interface: PageFragmentListener.java
public interface PageFragmentListener {
void onSwitchToNextFragment(String id);
}
Log: With attribute added to Manifest.xml
First instantiation
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: newInstance
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onCreate()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onCreateView()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: initLayout
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: setupRecyclerView()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onBindViewHolder
ClickItem
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onClick(): single pane
03-16 .../com.allison.viewpagermasterdetail D/MyAdapter: removed fragment
03-16 .../com.allison.viewpagermasterdetail D/MyAdapter: is instance of List Fragment
03-16 .../com.allison.viewpagermasterdetail D/ItemOneDetailFragment: newInstance
03-16 .../com.allison.viewpagermasterdetail I/Adapter: ItemPosition>>>FragmentB{41dd3810 #1 id=0x7f0c0069 android:switcher:2131492969:1}
03-16 .../com.allison.viewpagermasterdetail I/Adapter: ItemPosition>>>ItemListFragment{41df1528 #2 id=0x7f0c0069 android:switcher:2131492969:2}
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onPause()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onStop()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onDestroyView()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onDestroy()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onPause()
Back
03-16 .../com.allison.viewpagermasterdetail D/MyAdapter: removed fragment
03-16 .../com.allison.viewpagermasterdetail D/MyAdapter: is instance of Detail Fragment
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: newInstance
03-16 .../com.allison.viewpagermasterdetail I/Adapter: ItemPosition>>>FragmentB{41dd3810 #1 id=0x7f0c0069 android:switcher:2131492969:1}
03-16 .../com.allison.viewpagermasterdetail I/Adapter: ItemPosition>>>ItemOneDetailFragment{41e896a8 #2 id=0x7f0c0069 android:switcher:2131492969:2}
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onCreate()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onCreateView()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: initLayout
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: setupRecyclerView()
Rotate
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onConfigurationChanged
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: populateViewForOrientation
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: initLayout
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: setupRecyclerView()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onBindViewHolder
ClickItemLand
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onClick(): mTwoPane
Rotate
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onConfigurationChanged
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: populateViewForOrientation
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: initLayout
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: setupRecyclerView()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onBindViewHolder
ClickIemPortrait
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onClick(): single pane
03-16 .../com.allison.viewpagermasterdetail D/MyAdapter: removed fragment
03-16 .../com.allison.viewpagermasterdetail D/MyAdapter: is instance of List Fragment
03-16 .../com.allison.viewpagermasterdetail D/ItemOneDetailFragment: newInstance
03-16 .../com.allison.viewpagermasterdetail I/Adapter: ItemPosition>>>FragmentB{41dd3810 #1 id=0x7f0c0069 android:switcher:2131492969:1}
03-16 .../com.allison.viewpagermasterdetail I/Adapter: ItemPosition>>>ItemListFragment{41ea8b68 #2 id=0x7f0c0069 android:switcher:2131492969:2}
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onPause()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onStop()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onDestroyView()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onDestroy()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onPause()
Log: Without attribute
First instantiation
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: newInstance
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onCreate()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onCreateView()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: initLayout
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: setupRecyclerView()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onBindViewHolder
ClickItem
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onClick(): single pane
03-16 .../com.allison.viewpagermasterdetail D/MyAdapter: removed fragment
03-16 .../com.allison.viewpagermasterdetail D/MyAdapter: is instance of List Fragment
03-16 .../com.allison.viewpagermasterdetail D/ItemOneDetailFragment: newInstance
03-16 .../com.allison.viewpagermasterdetail I/Adapter: ItemPosition>>>FragmentB{41dd4758 #1 id=0x7f0c0069 android:switcher:2131492969:1}
03-16 .../com.allison.viewpagermasterdetail I/Adapter: ItemPosition>>>ItemListFragment{41ded998 #2 id=0x7f0c0069 android:switcher:2131492969:2}
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onPause()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onStop()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onDestroyView()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onDestroy()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onPause()
Back
03-16 .../com.allison.viewpagermasterdetail D/MyAdapter: removed fragment
03-16 .../com.allison.viewpagermasterdetail D/MyAdapter: is instance of Detail Fragment
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: newInstance
03-16 .../com.allison.viewpagermasterdetail I/Adapter: ItemPosition>>>FragmentB{41dd56b8 #1 id=0x7f0c0069 android:switcher:2131492969:1}
03-16 .../com.allison.viewpagermasterdetail I/Adapter: ItemPosition>>>ItemOneDetailFragment{41e830b8 #2 id=0x7f0c0069 android:switcher:2131492969:2}
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onCreate()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onCreateView()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: initLayout
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: setupRecyclerView()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onBindViewHolder
Rotate
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onPause()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onSaveInstanceState()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onStop()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onDestroyView()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onDestroy()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onPause()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onCreate()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onCreateView()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: initLayout
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: setupRecyclerView()
ClickItemLand
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onClick(): mTwoPane
Rotate
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onPause()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onSaveInstanceState()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onStop()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onDestroyView()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onDestroy()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onPause()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onCreate()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onCreateView()
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: initLayout
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: setupRecyclerView()
03-16 .../com.allison.viewpagermasterdetail E/ViewRootImpl: sendUserActionEvent() mView == null
03-16 .../com.allison.viewpagermasterdetail D/ItemListFragment: onBindViewHolder
ClickIemPortrait
// nothing happens
Using the MVP pattern might help since Presenters can be created in a way such that they survive configuration changes. I recently came across a series of blog posts aimed at explaining such an implementation of the MVP pattern. I'm sure they'll be of help to you.
Blogs posts: Part 1 Part 2
You seem to have several issues needed to be fixed.
First your link to
mListener
inSimpleItemRecyclerViewAdapter
gets broken when activity is recreated. Hence you need to restore that connection after activity is recreated. To do that you need to do following fixes.Main Activity
Make the
PageFragmentListener
accessible to outside by declaring it as a propertyItemListFragment
Override
onActivityCreated
method inItemListFragment
and restoremListener
by accessing it from the activityNow your
mListener
will be always set correctly when activity is recreated. But you have to do few more fixes for smoother operation.Your
MyAdapter
keep reference to an instance of fragment calledmFragment
. When activity recreated you need to restore this variable as well. Hence you need to modify yourMyAdapter
constructor as below.At this point your code should work. But it will crash when you rotate your device while viewing the detail of a list item. This happens because you add child fragments to the activity directly from your
ItemListFragment
using the Activity'sFragmentManager
. Instead use childFragmentManager from Fragment itself.