How to create Master - Detail fragments in Android

A fragment is an independent Android component which can be used by an activity. A fragment encapsulates functionality so that it is easier to reuse within activities and layouts. Introduction to fragments you can read here.

Fragments simplify the reuse of components in different layouts, e.g., you can build single-panel layouts for handsets (phones) and multi-pane layouts for tablets. This is not limited to tablets; for example, you can use fragments also to support different layout for landscape and portrait orientation on a smartphone.

To increase reuse of fragments, they should not directly communicate with each other. Every communication of the fragments should be done via the host activity.

For this purpose a fragment should define an interface as an inner type and require that the activity, which uses it, must implement this interface. This way you avoid that the fragment has any knowledge about the activity which uses it. In its onAttach() method it can check if the activity correctly implements this interface.

In demo application I use one main activity and one detailed activity. On a tablet the main activity contains both fragments in its layout, on a handheld it only contains the main fragment.

android_frg.png

Key points

  • usage of ListFragment
  • show details in activity or detail fragment, in dependency of screen orientation
  • set Toolbar as ActionBar
  • passing arguments to fragment

Full code you can see on github.

Model class

import java.util.ArrayList;
import java.util.List;

public class Movie {
    private String title;
    private Integer year;
    public static List<Movie> items = new ArrayList<>();

    public static void generate() {
        items.add(new Movie("The Shawshank Redemption", 1994));
        items.add(new Movie("The Godfather ", 1972));
        items.add(new Movie("The Godfather: Part II ", 1974));
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public Integer getYear() {
        return year;
    }

    public void setYear(Integer year) {
        this.year = year;
    }

    public Movie(String title, Integer year) {
        this.title = title;
        this.year = year;
    }
}

Main activity

import android.support.v4.app.FragmentTransaction;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;

public class MainActivity extends AppCompatActivity implements MovieListFragment.MovieListListener {
    private Toolbar toolbar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // creating toolbar and setting it as actionbar for the activity
        toolbar = (Toolbar) findViewById(R.id.toolbar);
        toolbar.setTitle(" IMDB");
        toolbar.setLogo(android.R.drawable.ic_dialog_info);
        toolbar.setTitleTextColor(0xFFFFFFFF);
        setSupportActionBar(toolbar);

        if (savedInstanceState == null) {
            Movie.generate();

            // set list fragment
            MovieListFragment frg = new MovieListFragment();
            getSupportFragmentManager().beginTransaction().add(R.id.flMovieList, frg).commit();

            // set detail fragment
            if (findViewById(R.id.flMovieDetail) != null) {
                Bundle arguments = new Bundle();
                arguments.putInt(MovieDetailFragment.ARG_ITEM_POS, 0);
                MovieDetailFragment fragment = new MovieDetailFragment();
                fragment.setArguments(arguments);
                getSupportFragmentManager().beginTransaction().add(R.id.flMovieDetail, fragment).commit();
            }
        }
    }

    @Override
    public void itemClicked(int pos) {
        if (findViewById(R.id.flMovieDetail) == null) {
            // start detail activity
            Intent detailIntent = new Intent(this, MovieDetailActivity.class);
            detailIntent.putExtra(MovieDetailFragment.ARG_ITEM_POS, pos);
            startActivity(detailIntent);
        } else {
            // replace detail fragment
            Bundle arguments = new Bundle();
            arguments.putInt(MovieDetailFragment.ARG_ITEM_POS, pos);
            MovieDetailFragment fragment = new MovieDetailFragment();
            fragment.setArguments(arguments);

            FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
            ft.replace(R.id.flMovieDetail, fragment);
            ft.addToBackStack(null);
            ft.commit();
        }
    }

}

Model adapter

import android.app.Activity;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

import java.util.List;

public class MovieAdapter extends BaseAdapter {
    private Context context;
    private List<Movie> movies;

    public MovieAdapter(Context context, List<Movie> movies) {
        this.context = context;
        this.movies = movies;
    }

    @Override
    public int getCount() {
        return movies.size();
    }

    @Override
    public Object getItem(int position) {
        return movies.get(position);
    }

    @Override
    public long getItemId(int position) {
        return movies.indexOf(getItem(position));
    }

    private class ViewHolder{
        TextView tvTitle;
        TextView tvYear;
    }

    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder = null;
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
        Movie movie = movies.get(position);

        if (convertView == null) {
            convertView = inflater.inflate(R.layout.movie_list_item, null);

            holder = new ViewHolder();
            holder.tvTitle = (TextView) convertView.findViewById(R.id.tvTitle);
            holder.tvYear = (TextView) convertView.findViewById(R.id.tvYear);

            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        holder.tvTitle.setText(movie.getTitle());
        holder.tvYear.setText(movie.getYear().toString());

        return convertView;
    }
}

Detail activity (host for detail fragment)

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MovieDetailActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.movie_detail_activity);

        if (savedInstanceState == null) {
            MovieDetailFragment fragment = new MovieDetailFragment();
            Bundle arguments = new Bundle();

            arguments.putInt(MovieDetailFragment.ARG_ITEM_POS, getIntent().getIntExtra(MovieDetailFragment.ARG_ITEM_POS, 0));
            fragment.setArguments(arguments);

            getSupportFragmentManager().beginTransaction().add(R.id.flMovieDetail, fragment).commit();
        }

    }

}

Detail fragment

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class MovieDetailFragment extends Fragment {
    public static final String ARG_ITEM_POS = "moviePOS";
    private Movie movie;

    public MovieDetailFragment() {}

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (getArguments().containsKey(ARG_ITEM_POS)) {
            movie = Movie.items.get(getArguments().getInt(ARG_ITEM_POS));
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.movie_detail_fragment, container, false);

        if (movie != null) {
            TextView tvTitle = (TextView)v.findViewById(R.id.tvTitle);
            tvTitle.setText(movie.getTitle());

            TextView tvYear = (TextView)v.findViewById(R.id.tvYear);
            tvYear.setText(movie.getYear().toString());
        }

        return v;
    }

}

List fragment

import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.Toast;

public class MovieListFragment extends ListFragment {
    MovieAdapter adapter;

    static interface MovieListListener {
        void itemClicked(int pos);
    };

    private MovieListListener listener;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.movie_list_fragment, container, false);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        adapter = new MovieAdapter(getActivity(), Movie.items);
        setListAdapter(adapter);
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);
        getActivity().getMenuInflater().inflate(R.menu.movie_list_menu, menu);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setHasOptionsMenu(true);
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        this.listener = (MovieListListener)activity;
    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        Movie item = Movie.items.get(position);

        Toast.makeText(getActivity(), item.getTitle(), Toast.LENGTH_SHORT).show();

        if (listener != null) {
            listener.itemClicked(position);
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // handle item selection
        switch (item.getItemId()) {
            case R.id.some_item:
                Toast.makeText(getActivity(), "Some action", Toast.LENGTH_LONG).show();
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }

}
comments powered by Disqus