Insider's Guide To Udacity Android Developer Nanodegree Part 3 - Making the Baking App
Written by Nikos Vaggalis   
Monday, 03 July 2017
Article Index
Insider's Guide To Udacity Android Developer Nanodegree Part 3 - Making the Baking App
Step 1 Fragments
Step 2 - Libraries & Networking
Step 3 - Adding Exoplayer
Step 4 - Widgets
Step 5 - The Widget Provider
Step 6 - UI Testing
Step 7 - Testing Intents


In order to have such modular design we have to turn our attention to fragments. And that is the big deal with fragments, as self contained pieces of UI they can be combined into a single activity or reused as single fragments in multiple activities. So on a Phone device I'm going to use two activities, RecipeActivity and RecipeDetailActivity. RecipeActivity is going to host a RecipeFragment which in turn hosts a RecyclerViewer containing the titles of the available recipes.

image6

Phone/Portrait,RecipeActivity/RecipeFragment


Upon pressing/clicking on a recipe, a new activity should open, the RecipeDetailActivity which should contain another fragment, the RecipeDetailFragment which in turn should host two other views. A scrollable TextView containing the list of the chosen recipe's ingredients and underneath it a RecyclerViewer with a CardView layout which card elements contain the titles of the associated steps that are necessary to execute the recipe.

image7

Phone/Portrait,RecipeDetailActivity/RecipeDetailFragment


Upon pressing/clicking on a step, the RecipeDetailActivity should stay in place but replace the RecipeDetailFragment with the RecipeStepDetailFragment which should contain the video with Miriam demonstrating how to execute the chosen step. Underneath the video there should be the TextView with the a short description.

image8

Phone/Portrait,RecipeDetailActivity/RecipeStepDetailFragment
 

 

In contrast, when on a Tablet device the launcher activity, RecipeActivity. should act as the Tablet's did:

image9Tablet/Landscape,RecipeActivity/RecipeFragment

 

but the RecipeDetailActivity should host both RecipeDetailFragment and RecipeStepDetailFragment side by side, in order to implement the Master-Detail flow. Again, that's the magic of fragments, in allowing for UI component reuse as deemed necessary.

 

image10

Tablet/Landscape,RecipeDetailActivity/RecipeDetailFragment
/RecipeStepDetailFragment

Since the RecipeFragment won't be changing during the runtime of its host RecipeActivity, we can consider it a Static Fragment, which means that we can treat it and load it as a simple design time layout like every other.The two other fragments however, RecipeDetailFragment and RecipeStepDetailFragment, are added and replaced by the FragmentManager (the component burdened with this sort of tasks) during the runtime of their respective host RecipeDetailActivity, as such they are considered dynamic and will instead be enclosed by and loaded into their so called Fragment Containers, fragment_container and fragment_container2.

Each device has its own layouts, so on a Tablet we should load both fragment_container (for RecipeDetailFragment) and fragment_container2 (for RecipeStepDetailFragment) side by side, while on a Phone only fragment_container (for RecipeDetailFragment, which gets replaced by RecipeStepDetailFragment). Hence we need a way in code to tell case from case.The trick here is that each case has its own dedicated layout stored in the respective subfolder underneath the project's 'res' folder.

So inside the default 'layout' folder we have the one size fits all 'activity_recipe_detail.xml' layout that contains the single fragment_container, while inside the 'layout-sw600dp-land' folder we have  the specially crafted 'activity_recipe_detail.xml' layout that contains both fragment_container and fragment_container2.sw600 stands for smallest width and 600dp denotes the Tablet's dimensions.


image12

 

So while the layouts differ, RecipeDetailFragment's and RecipeStepDetailFragment's underlying code remains unaltered, but should be developed to cater for each case.This can be done at runtime by checking which kind of layout is currently loaded.So when on a Phone, the layouts under the default 'layout' folder get loaded, while on a Tablet and in landscape mode those in 'layout-sw600dp-land' are used.

A good and efficient way to identify which is which is by tagging them. So 'activity_recipe_detail.xml' of 'layout-sw600dp-land' is tagged with android:tag="tablet-land" something that allows for the following check at runtime:


/****
RecipeDetailActivity
****/

if (findViewById(R.id.recipe_linear_layout).getTag()!=
    null  &&    findViewById(R.id.recipe_linear_layout)
            .getTag().equals("tablet-land"))
 {
  final RecipeStepDetailFragment fragment2 =
                                            new  RecipeStepDetailFragment();
  fragment2.setArguments(selectedRecipeBundle);
  fragmentManager.beginTransaction()
                 .replace(R.id.fragment_container2, fragment2)
                 .addToBackStack(STACK_RECIPE_STEP_DETAIL)
                 .commit();
    }
   

 

So if the layout currently loaded is tagged as "tablet-land" then I know that that's 'activity_recipe_detail.xml' of  'layout-sw600dp-land' and therefore I can follow the multi pane branch of code. Note the use of the FragmentManager which is the class that provides the methods that allow you to add, remove, and replace fragments in an activity at runtime.

Fragments have their own lifecycle too, although connected to their host's activity lifecycle so that when the hosting activity goes through its own callbacks, onCreate, onStart, onPause, onDestroy, onSaveInstanceState and so on, so do the fragments. However they also come with extra callbacks in onAttach, onDetach and onDestroyView.

Yet, another property of fragments is their ability to push onto the fragment stack, which I'm going to use for implementing the Back Arrow's functionality.

The instances of the FragmentManger we saw earlier use the addToBackStack method in 

addToBackStack(STACK_RECIPE_DETAIL)

and

addToBackStack(STACK_RECIPE_STEP_DETAIL)

pushing the fragment instances onto the stack one after another and in LIFO fashion.

Now when pressing the toolbar's back arrow we pop them off the stack in the following order. When the user stares at the screen with the Video instructions, which means that the RecipeStepDetailFragment is loaded, pressing the back arrow takes him back to the screen with the list of the recipe's steps, popping RecipeStepDetailFragment off the stack and replacing it with RecipeDetailFragment. At the RecipeDetailFragment screen, pressing the back arrow again ends host RecipeDetailActivity by calling finish() and goes back to the launcher RecipeActivity so that the user can check another recipe.


/****
RecipeDetailActivity Back Arrow
****/

 myToolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                FragmentManager fm = getSupportFragmentManager();
                if (findViewById(R.id.fragment_container2)==null) {
                    //called when fragment_container2 exists,
                    //that is both fragments are side by side
                    if (fm.getBackStackEntryCount() > 1) {
                        //go back to "Recipe Detail" screen
                        fm.popBackStack(STACK_RECIPE_DETAIL, 0);
                    } else if (fm.getBackStackEntryCount() > 0) {
                        //go back to "Recipe" screen
                        finish();
                    }
                }
                else {
                    //go back to "Recipe" screen
                    finish();
                }
            }
        });
    }


Surprisingly the Backstack and its functionality are not covered in the lesson, therefore you have to somehow suspect that something like that first of all exists and then try to figure out how it works by relying on external resources.

Fragment to Fragment and Fragment to host Activity Communication

It's important to note that all communication, be it Fragment to Fragment or Fragment to Activity, is governed by the Control and Command center of the host Activity.That is so because our fragments should be as self contained as possible in order to be modular and reusable. As such letting fragments call into each other would defy this purpose and unnecessarily raise the application's complexity. Going with the flow, the action of choosing a recipe to check from within the RecipeActivity packs the associated Recipe object, selectedRecipe, with its associated Ingredients and Steps lists in a ParcelableArrayList and forwards it to the RecipeDetailActivity which in turn forwards to its fragment(s).


But communication is bi-directional as well, not just from Activity to Fragment but also from Fragment to Activity.This part is coded deep into each fragment's RecyclerView Adapters.For example RecipeDetailFragment's RecipeDetailAdapter which holds the list of the steps associated with the chosen recipe, has an Interface declaration, ListItemClickListener with just one method, onListItemClick.


/****
RecipeDetailAdapter-ListItemClickListener interface
****/

 public interface ListItemClickListener {
        void onListItemClick(List<Step> stepsOut,
                  int clickedItemIndex,String recipeName);
    }
   
    @Override
        public void onClick(View v) {
            int clickedPosition = getAdapterPosition();
            lOnClickListener.onListItemClick(lSteps,
                                                 clickedPosition,recipeName);
        }
       


So upon clicking on a recipe step through the associated RecyclerViewer UI, we trigger the RecyclerViewer's Adapter's onClick method, which in turn reads the whole list of the steps (lSteps) of the given recipe as well as the chosen step's index in the List of steps, lSteps.This means that any interested listener given both of these pieces of information can locate the step within the accompanying list and extract it. And who might that listener be? The fragment's host activity, RecipeDetailActivity, of course, since RecipeDetailActivity  
implements the RecipeDetailAdapter.ListItemClickListener interface: 

  
 public class RecipeDetailActivity extends AppCompatActivity
             implements RecipeDetailAdapter.ListItemClickListener{}


So, with its own onListItemClick method:

   
public void onListItemClick(List<Step> stepsOut,
                         int selectedItemIndex,String recipeName)


is able to capture the list of step objects 'stepsOut' and the associated index in 'selectedItemIndex', sent over from the RecipeDetailAdapter of the RecipeDetailFragment. Acting as the controller of information flow, it turns the very same info:      

  
 fragment.setArguments(stepBundle)


over to RecipeStepDetailFragment with:   


replace(R.id.fragment_container,fragment)
          .addToBackStack(STACK_RECIPE_STEP_DETAIL) 

in order to show the video of Miriam going over the chosen step's instructions.


The following diagram will make the flow easier to understand:

image13

Information flow


With this final step I fulfilled the specification requirements 'App should allow navigation between individual recipes and recipe steps' and 'Application uses Master Detail Flow to display recipe steps and navigation between them' .


 



Last Updated ( Monday, 20 November 2017 )