Case Study. Master/Detail Pattern Revisited

This article is part of my responsive design series. I’m going to present a case study of my workflow on a slightly more advanced layout. How to make a base template to have something to easily work with in the future, to add new features etc. In this one, I will showcase a classic design pattern – Master/Detail. I’m pretty sure you are familiar with this one (if no click) so I wanted to fancy it up and add some of the Material Design goodness. Hope you will like it.

TL;DR
Before we start, as usual, you will find all the source code on my Github, and can try how it feels under your fingertips by downloading a sample from Play Store.

Let’s begin!

Specification

I am developing a base level navigation for imaginary social network App.
There will be a lot of main entry points, so I’ve chosen NavigationDrawer as main navigation pattern. One of the screens includes a list of friends a User is connected with on the platform. He can scan the list and open another screen which includes all the details about that person. This three elements (side navigation, the master screen with a list and the detail screen) will be the main subject of this article.

I want the design to look beautifully on different screen sizes.  Can not allow a single list with rows of people to stretch 1000dp wide on tablets or desktop window, so let’s start with the following mockup.

specification

As you can see it looks pretty simple on mobile. Just a list with toolbar on top, and detail screen with a cover image. On a tablet, it’s more interesting. There are two sheets of material, one for the list and one for the detail, on top of that an expanded Toolbar. The order in Z axis is as follows: a List, expanded Toolbar and Detail.
Actually, if you think about it, it’s the same approach in both cases. On mobile, it’s just a smaller toolbar and edges of material sheets can’t be seen.

This is the same design with a different perspective.

tablet_iso

mobile_iso

Material Design is all about finding those real-life relations between abstract, sci-fi like material and build on top of that.

Breakpoints and Middle States

Right now we understand the main concept. In the App will be two frames to inflates fragments into. One on the left and very low in Z order, One on the right and height in Z order. A toolbar in between which can shape two forms, a regular one, or expanded – with doubled height and split actions.

Let’s define more rules. There are more things happening between 360dp wide on mobile and 1024dp wide on a tablet.
I have to specify breakpoints, max widths, and some major margins.
This is the time when I can actually start coding, or at least, planning to code.
In this case, I’ve specified the first breakpoint for w-600dp (600dp wide). I’m switching layout of my CustomToolbar (we will get back to it later) to show it in expanded mode. I’m also switching layout inside of my ContainersLayout to use views with card background and specified max width. The second breakpoint is w-840dp – to know if it’s possible to lay out two sheets next to each other in Master/Detail flow.

redlines_360

↑ Window width: 360dp;

redlines_600

↑ Window width: 600dp; – First breakpoint! Permanent items from NavigationDrawer on left side. Expanded Toolbar. Container with card background, max-width: 540dp with 72dp margins on left and right.

redlines_839

↑ Window width: 839dp; – The widest configuration before another breakpoint. Container in full 540dp width.

redlines_840

↑ Window width: 840dp; – Second breakpoint! Two Containers with card backgrounds next to each other with max-width: 540dp and 72dp margins on left and right.

redlines_1280

↑ Window width: 1280dp; – The widest configuration we are taking into account. Two Containers fully expanded to 540dp.

Dimensions

main/res/values/dimens.xml:
    <!-- Default screen margins, per the Android Design guidelines. -->
    <dimen name="horizontal_margin">16dp</dimen>
    <dimen name="horizontal_keyline">72dp</dimen>

    <!-- Container Sheet -->
    <dimen name="container_max_width">600dp</dimen>
    <dimen name="container_horizontal_margin">0dp</dimen>

main/res/values-w600dp/dimens.xml:
    <!-- Default screen margins, per the Android Design guidelines. -->
    <dimen name="horizontal_margin">24dp</dimen>
    <dimen name="horizontal_keyline">80dp</dimen>

    <!-- Container Sheet -->
    <dimen name="container_max_width">540dp</dimen>
    <dimen name="container_horizontal_margin">72dp</dimen>

 

Views and Layouts of the setup

It’s always a good idea to keep as few different versions of one layout as possible and operate only with different values from dimensions. Ideally, a setup with 2 breakpoints should have maximum 3 layout configurations. In this example, I wanted to have more granular control so the main parts are two custom views: CustomToolbarContainersLayout and main activity layout.

CustomToolbar encapsulates all logic connected with Toolbar. It displays an expanded view in layout-w600dp configuration. This expanded view actually contains two Toolbars on top of each other, knows which one should render the title and how to split menu actions. All views should be self-aware of the current configuration. To achieve it, I’m simply trying to find child view which exists only in specific one. For that reason, CustomToolbar inflates also configuration from layout-w840dp with Space on the right side to not be covered by DetailContainer.

ContainersLayout has a similar function as the previous view. Takes care of laying out frame containers on top of each other, or next to each other depending on window width. In layout-w600dp it adds CardView as background with left and right margins and layout-w840dp specifies two-column flow.

Architecture underneath

Template is written on top of simple MVP architecture. I like to specify one Contract per screen/fragment to have all public methods clearly visible in one file. For example:

public interface PeopleContract {
    interface Navigator extends BaseNavigator {
        void goToPersonDetails(Person person);
    }

    interface View extends BaseView {
        void showLoading();
        void hideLoading();
        void showPeopleList(List<Person> peopleList);
        void showToast(String message);
    }

    interface Presenter extends BasePresenter<PeopleContract.View> {
        void getPeople();
        void clickPerson(Person person);
        void clickPersonAction(Person person);
        void loadMorePeople();
    }
}

View should be as dumb as possible, should only render models provided by Presenter and pass input in reverse. Presenter holds the logic, connects with repositories and uses Navigators to go to other screens. By itself, Presenter also doesn’t know how the next screen will be opened. The mayor logic about creating Fragments or Activites, starting or committing them into frame containers, lays inside MainNavigator. MainNavigator is an implementation of MainContract.Navigator which together with MainContract.Presenter and MainContract.View make the base layer of this templates and in general are responsible for ContainersLayout and NavigationDrawer.

public class MainNavigator implements MainContract.Navigator {
    ...
    @Inject
    public MainNavigator(MainActivity mainActivity) {
        this.mainActivity = mainActivity;
    }

    @Override
    public void goToPeople() {
        clearDetails();
        //custom view lays out itself based on State and xml configuration it is aware of
        mainActivity.getCustomAppBar().setState(State.TWO_COLUMNS_EMPTY);
        mainActivity.getContainersLayout().setState(State.TWO_COLUMNS_EMPTY);
        PeopleFragment master = PeopleFragment.newInstance();
        mainActivity.getSupportFragmentManager()
                 .beginTransaction()
                 .replace(R.id.activity_main__frame_master, master, TAG_MASTER)
                 .commitNow();
    }

    @Override
    public void goToPersonDetails(Person person) {
        //custom view lays out itself based on State and xml configuration it is aware of
        mainActivity.getCustomAppBar().setState(State.TWO_COLUMNS_WITH_DETAILS);
        mainActivity.getContainersLayout().setState(State.TWO_COLUMNS_WITH_DETAILS);
        PersonDetailsFragment fragment = PersonDetailsFragment.newInstance(person);
        mainActivity.getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.activity_main__frame_details, fragment, TAG_DETAILS)
                .commitNow();
    }

    @Override
    public void goToSettings() {
        //start new activity
    }

    @Override
    public boolean onBackPressed() {
        //simulate poping backstack when not in two columns State
        return false;
    }
}

 

This MainNavigator needs access to mainActivity. It’s done by dependency injection using Dagger2. Actually, this template uses Dagger2 a lot. You can see above public method goToPersonDetails(), but it’s really PeopleContract.Navigator‘s responsibility to open PersonDetails screen. It’s done by delegating this method to MainContract.Navigator.

public class PeopleNavigator implements PeopleContract.Navigator {
    private MainContract.Navigator mainNavigator;

    @Inject
    public PeopleNavigator(MainContract.Navigator mainNavigator) {
        this.mainNavigator = mainNavigator;
    }

    @Override
    public void goToPersonDetails(Person person) {
        mainNavigator.goToPersonDetails(person);
    }
}

It’s important to pay attention to proper scoping. MainNavigator is tight to Activity lifecycle, so PeopleNavigator’s life must be shorter than it’s delegate. It’s done be setting in Dagger2 MainComponent with custom @ActivityScope, and PeopleComponent as a subcomponent with @FragmentScope.

@ActivityScope
@Component(
        dependencies = {ApplicationComponent.class},
        modules = {MainModule.class}
)
public interface MainComponent {
    void inject(MainActivity activity);
    PeopleComponent plus(PeopleModule peopleModule);
}

...

@FragmentScope
@Subcomponent(
        modules = {PeopleModule.class}
)
public interface PeopleComponent {
    void inject(PeopleFragment fragment);
}

Summary

While I could go through even more details and try to explain deeply how different parts work together, but I think I’ve covered the major ones, and right now you get the idea, what’s inside this demo template. I encourage you to take a look at the source code. Hope you will find something interesting. Feel free to use it as a base for your next project, or just cut it in pieces and take whatever suits you.

Again, all the sources can be found on Github and you can quickly check the final result by downloading a sample from Play Store.

If you liked this article, maybe you will also like the next one. Consider subscribing to the newsletter to be notified.