Google leanback - big brother care

Good afternoon, readers of Habr. I would like to share my experience in developing an application for Android TV using the DetailsFragment example.


There are official examples here and official documentation here . What motivated me to express my opinion? This is because official examples do not meet modern development requirements, for example, modularity and extensibility. Sometimes a certain duality is created when using one or another mechanism. Let's take a closer look at DetailsFragment.

In order to start developing your application for the android platform, in my opinion, you should accept 2 basic truths:

  • It’s a bad idea to move away from official recommendations and develop a custom application. Google made sure that this was extremely difficult.
  • Single Activity Architecture also does not quite fit, it is fraught with memory leaks associated with internal leanback library implementations.

So, first things first:

Briefly about the Leanback library


The Leanback library is a collection of screen templates with various functional features. There are screens for displaying lists, content cards, dialogs, etc. These screens process all user transitions between elements and animations, and also have quite extensive functionality for building simple out-of-box applications. The ideology of this library is that all applications based on it should be similar in terms of use. My personal opinion is a pretty good idea of ​​creating uniformity of applications on the market. I don’t have to think anymore, will the user know that I can scroll down? He finds out because he has already used hundreds of similar applications.

But, like in a large number of libraries connected to the project, there are sharp inconsistencies between customer expectations of the product and the capabilities of this library. Using one example, I will try to draw two main conclusions that I have drawn for myself while developing this application.

So the DetailsFragment class


This class is used to display the "content card". Content cards are a screen space that displays complete information about a particular entity object. In most cases, when the user clicks something in the list of similar entity objects, he gets to the content card.

What is DetailsFragment


This class is a collective image of user views. The logical structure is as follows:
Let us examine in order what element does what.

  • ArrayObjectAdapter is a class that collects all the elements on the screen.
  • DetailsOverviewRow - part of the main adapter, is responsible for displaying functional elements (Actions), for information (DescriptionView) and a picture of a content card.
  • Additional Row - additional elements that expand the functionality of the content card fall into this row.

And so at first glance it’s all just that we have some kind of “nested doll” in which roles are clearly distributed (in fact, no, we will see this later).

I will dwell only on the basic concepts of DetailsOverviewRow, because they are really quite interesting in my opinion.

The DetailsOverviewRow class has the following main methods:

  • setImageDrawable (Drawable drawable) - this method sets the "avatar" of our content card. An alternative method for setting an avatar can be setImageBitmap (Context context, Bitmap bm).
  • void setActionsAdapter (ObjectAdapter adapter) - installation of an adapter for content card events (for example, buy / watch / add to favorites and so on). ObjectAdapter is an abstract class. In leanback, there are several implementations on classic structures (e.g. ArrayObjectAdapter). We can add different classes to our ObjectAdapter, in this case we can resort to the standard Action class.
  • To set DescriptionView, use the constructor of the DetailsOverviewRow class, which accepts a certain model.

I wonder how DetailsOverviewRow puts our model with information to display? Where is the markup for this display taken?

Presenter (not classic MVP)


It just so happened that Google named the class that is responsible for how this or that model looks in the framework of internal views, presenter. Further, for convenience, I will call it a UI presenter.

So, UI presenters, this is essentially how our data objects or entity objects get onto the screen. If you draw an analogy with the classic android development, the adapter is all about it.

In the case of DescriptionView, we need to create a ui presenter that will put the model on a given user view. There are 2 main ways we can do this:

  • FullWidthDetailsOverviewRowPresenter - This class is a descendant of RowPresenter. It is a full-screen display of DetailsOverviewRow. Appearance:

  • DetailsOverviewRowPresenter - No longer supported. Displayed as in the picture below


As a result, we select FullWidthDetailsOverviewRowPresenter, since there are no alternatives out of the box.

Creating a presenter ui will look like this:

FullWidthDetailsOverviewRowPresenter rowPresenter = 
new FullWidthDetailsOverviewRowPresenter(
                new DetailsDescriptionPresenter(context))

DetailsDescriptionPresenter is a class that extends from Presenter. It is responsible for the user display of the entity object (most often it indicates the name and description).

As mentioned earlier, the ui presenter is an analogue of the adapter in the classic android. The following methods are required:

  • ViewHolder onCreateViewHolder (ViewGroup parent) - this method is designed to create a ViewHolder object. Here we can create our custom view and pass it to the ViewHolder.
  • void onBindViewHolder (ViewHolder viewHolder, Object item) - a method of constructing our ViewHolder. As you can see, this is a bit slippery situation, since the data object is passed as a java object. You may receive a runtime error if you use the downstream conversion incorrectly.
  • void onUnbindViewHolder (ViewHolder viewHolder) - this method is used to free our holder from resources so that the garbage collector can safely delete it.

Overall picture


Throughout the android TV project, you will use ArrayObjectAdapter with custom presentations, you will probably use presenter factories. It’s worth remembering that they are simply embedded in each other and in the implementation of a particular screen give some form of presentation. For example, I created my own presenter ui class, named it AbstractCardPresenter. This class has helped me more than once since it smooths out irregularities with transformations at the level of their appearance. Also created a basic presentation of cards. This helped me reuse ready-made views where they are required and partially customize the cards.

AbstractCardPresenter
public abstract class AbstractCardPresenter<T extends BaseCardView> 
extends Presenter {

private static final String TAG = "AbstractCardPresenter";
private final Context mContext;
    
public AbstractCardPresenter(Context context) {
        mContext = context;
    }

public Context getContext() {
        return mContext;
    }

@Override 
public final ViewHolder onCreateViewHolder(ViewGroup parent) {
        T cardView = onCreateView();
        return new ViewHolder(cardView);
    }

@Override 
public final void onBindViewHolder(ViewHolder viewHolder, Object item) {
        Card card = (Card) item;
        onBindViewHolder(card, (T) viewHolder.view);
    }

@Override 
public final void onUnbindViewHolder(ViewHolder viewHolder) {
        onUnbindViewHolder((T) viewHolder.view);
    }

public void onUnbindViewHolder(T cardView) {
    }
    
protected abstract T onCreateView();
    
public abstract void onBindViewHolder(Card card, T cardView);

}


"a bad idea to move away from official recommendations"?


It is bad due to the fact that in classes that were carefully written for us, most of the methods are immutable for the simple reason of strong internal coherence. In order not to violate the internal state of the screen (in fact DetailsFragment and others are fragments), you should use them as intended. I will not go into details of the implementation of internal classes, state machine, and other ideas of the developers of this library. A real example from my work is the leak of the whole fragment when using Single Activity Architecture.

This leak was related to DetailsFragment transitions. Through trial and error, it was possible to find the cause of the leak, eliminate the leak and write a report in the bug. Given the low power of the TVs themselves (Sony Brawia 4K 2GB RAM), the OOM problem is quite acute. Leakage is eliminated by zeroing these transitions. When using transitions between activities, this problem was not observed.

TransitionHelper.setReturnTransition(getActivity().getWindow(), null);
TransitionHelper.setEnterTransition(getActivity().getWindow(), null);

Out of the box does not work!


If you really want (it requires the customer) to change this or that display, this can be done, I will tell you with an example that I came across. For my experience in developing for android tv, I have seen many constraints: it is impossible to track internal fragments created by the library; their life cycle is not controlled by anyone; calls to create custom views in designers (asynchronous data cannot be used). Google did almost everything so that it was impossible to write "how to." Given modern requests, a non-flexible mechanism turns out to be bad and not needed, but since there are no alternatives (except for writing your own leanback), you have to live with what you have.

The first thing that attracted my attention to the implementation of the box is the avatar of the content card. When you switch the focus down and up, it absolutely unanimously twitches down.

Example


By narrowing down the search for classes that are responsible for this view, I headed to the implementation of the FullWidthDetailsOverviewRowPresenter class to find the answer to the question of how it moves. I managed to find a method that is responsible for moving the avatars of our content card - void onLayoutLogo (ViewHolder viewHolder, int oldState, boolean logoChanged).

The default implementation was as follows:

/**
     * Layout logo position based on current state.  Subclass may override.
     * The method is called when a logo is bound to view or state changes.
     * @param viewHolder  The row ViewHolder that contains the logo.
     * @param oldState    The old state,  can be same as current viewHolder.getState()
     * @param logoChanged Whether logo was changed.
     */
    protected void onLayoutLogo(ViewHolder viewHolder, int oldState, boolean logoChanged) {
        View v = viewHolder.getLogoViewHolder().view;
        ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams)
                v.getLayoutParams();
        switch (mAlignmentMode) {
            case ALIGN_MODE_START:
            default:
                lp.setMarginStart(v.getResources().getDimensionPixelSize(
                        R.dimen.lb_details_v2_logo_margin_start));
                break;
            case ALIGN_MODE_MIDDLE:
                lp.setMarginStart(v.getResources().getDimensionPixelSize(R.dimen.lb_details_v2_left)
                        - lp.width);
                break;
        }

        switch (viewHolder.getState()) {
        case STATE_FULL:
        default:
            lp.topMargin =
                    v.getResources().getDimensionPixelSize(R.dimen.lb_details_v2_blank_height)
                    - lp.height / 2;
            break;
        case STATE_HALF:
            lp.topMargin = v.getResources().getDimensionPixelSize(
                    R.dimen.lb_details_v2_blank_height) + v.getResources()
                    .getDimensionPixelSize(R.dimen.lb_details_v2_actions_height) + v
                    .getResources().getDimensionPixelSize(
                    R.dimen.lb_details_v2_description_margin_top);
            break;
        case STATE_SMALL:
            lp.topMargin = 0;
            break;
        }
        v.setLayoutParams(lp);
    }

The implementation is found, then I created an inheritor class FullWidthDetailsOverviewRowPresenter in which I redefined the onLayoutLogo method and wrote my implementation.

public class CustomMovieDetailsPresenter extends FullWidthDetailsOverviewRowPresenter {
	
	private int mPreviousState = STATE_FULL;
	
	public CustomMovieDetailsPresenter(final Presenter detailsPresenter) {
		super(detailsPresenter);
		setInitialState(FullWidthDetailsOverviewRowPresenter.STATE_FULL);
	}
	
	@Override
	protected void onLayoutLogo(final ViewHolder viewHolder, final int oldState, final boolean logoChanged) {
		final View v = viewHolder.getLogoViewHolder().view;
		final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
		
		lp.setMarginStart(v.getResources().getDimensionPixelSize(
			android.support.v17.leanback.R.dimen.lb_details_v2_logo_margin_start));
		lp.topMargin = v.getResources().getDimensionPixelSize(android.support.v17.leanback.R.dimen.lb_details_v2_blank_height) - lp.height / 2;
		
		switch (viewHolder.getState()) {
			case STATE_FULL:
			default:
				if (mPreviousState == STATE_HALF) {
					v.animate().translationY(0);
				}
				
				break;
			case STATE_HALF:
				if (mPreviousState == STATE_FULL) {
					final float offset = v.getResources().getDimensionPixelSize(android.support.v17.leanback.R.dimen.lb_details_v2_actions_height) + v
						.getResources().getDimensionPixelSize(android.support.v17.leanback.R.dimen.lb_details_v2_description_margin_top)+lp.height/2;
					v.animate().translationY(offset);
				}
				
				break;
		}
		mPreviousState = viewHolder.getState();
		v.setLayoutParams(lp);
	}
}

Result


In this case, everything was decided relatively simply, with the state when the screen goes down, we animatedly lower the content card avatar behind it. When you return to the starting position, we also animatedly shift the avatar up. But there are cases that the method is declared final or is inaccessible, then I resorted to reflection. Reflection is an extreme stage, as it has a number of disadvantages:

  • When updating the library, it may happen that the field that we accessed through reflection is renamed, or even worse, it is deleted
  • Reflection is a fairly resource-intensive operation, which often causes difficulties in the operating system
  • Reflection is hard to read and hard to maintain

In other words, reflection is the last resort to which I resorted once. But I remember how - the mechanism.

A bit about layered architecture in an android tv application


In this case, everything is relatively simple, problems can arise only in the layer of user representations, since it is sometimes difficult to understand exactly where an element belongs. Returning to our example with DetailsFragment, the real tasks will be approximately the following: If the content is purchased, then display the "Watch" button; If the content is rented, then display the watch button + rental end time, etc .; With all this, there is a trailer button, a button to add to favorites, etc. In my opinion, a presenter (MVP) should receive some kind of model and call the addAction method (android.support.v17.leanback.widget.Action action). That is, the presenter, on the basis of the data, concludes which buttons should be added, generates them, and calls the corresponding external interface view method. Here the problem of the presenter's dependence on the leanback library appears. Since, for good, you need to use this presenter in other parts of our program, for example on a mobile device, the problem arises rather sharply. Thus, I introduced the rule for developing presenters in a project in which I participate - not to declare implicit dependencies in the presenter that are tied to the framework.

To avoid this, it was decided in presenters to use the local analog of android.support.v17.leanback.widget.Action. This solved a lot of problems in the presenter, but it generated twofold logic in the view related to processing the position of adding and processing clicks, since in view we can very well operate on widgets provided by leanback. The same double logic appears when the set of buttons is unknown initially, but they have certain priorities. For example, the “watch” button should be in front of the trailer button, the buy button should be after the trailer, etc. Accordingly, a certain mechanism appears in the view that compares the identifiers of the buttons and their positions, which makes the identifier a kind of "priority to show". I circumvented this situation quite trivially, но опять же вью начинает приобретать логику и знает что это не просто идентификатор.

private final List<Integer> mActionsIndexesList = new ArrayList<>();
	
@Override
public void addAction(final MovieDetailAction movieDetailAction) {
	final Action action = new Action(movieDetailAction.getId(), 
            movieDetailAction.getTitle(), 
            movieDetailAction.getSubTitle(), 
            movieDetailAction.getIcon());
	actionAdapter.set(movieDetailAction.getId(), action);
	mActionsIndexesList.add(movieDetailAction.getId());
	Collections.sort(mActionsIndexesList);
	}

@Override
public void setSelectedAction(final int actionId) {
		new Handler().postDelayed(() -> 
mActionsGridView.smoothScrollToPosition(getActionPositionByActionId(actionId)), 100);
	}

	private int getActionPosition(final int actionId) {
		return mActionsIndexesList.indexOf(actionId);
	}

In custody


Application development for Android Tv is relatively new, and therefore interesting. At the time of writing, the Android Tv developer community is decentralized, and so most problems are solved by “trampling their tracks.” Also, in my opinion, programming in the framework of limited resources (RAM, processing power, etc.) is quite interesting. Such a pattern of thinking is not always characteristic of developers of classic android applications and in my opinion is a useful experience.