Exercise Thursday

Note that we strongly warn against using random code samples off StackOverflow or internet blogs prior to ~2015, especially where any kind of fragment is concerned. The way dialogs and timepickers are now implemented is fundamentally different and earlier approaches are deprecated. Being aware of the age of sources is especially important when researching and implementing code for any kind of fragment, such as DialogFragment, DatePickerFragment, TimePickerFragment, and others. Proceed with caution!

Warm Up


  • What is the difference between the ‘system back button’ and the ‘up’ button?
  • What is a hierarchical parent? Where do we denote an activity's parent? What does creating this relationship allow us to do?
  • What is the Android Manifest? What is placed in here?

Practice


Comparing web apps to mobile apps

By now we should be feeling fairly comfortable working with Activities in Android, even if we are still needing a lot of time to accomplish simple things - you are learning a lot of new content this week and that can be overwhelming!

Sometimes it can be useful to zoom out and put things into a little perspective to make the bigger picture easier to comprehend.

Let's take a minute to do that before we move on.

In our apps, as well as in any kind of web application (and the overwhelming majority of other kind of apps as well), we are trying to respond to one specific kind of circumstance:

A user uses our app or website to achieve some kind of goal, such as seeing a list of products, tracking her todo’s, sending a message to a friend, or playing a game.

If we consider what we develop from this perspective, the central problems we have to solve are always the same:

  • We need to capture our users intent correctly.
  • We need to process that intent in some way, in accordance with our own processing code that allows us to store a to-do, save a score to a highscore, or send a message.
  • We need to then update our app dependent on the outcome of that intent, updating our user interface, so that our user can make a decision based on this updated set of circumstances.

Conclusion:

These overarching principles are the same regardless of what type of app we are trying to create, and also which platform we are trying to create that app on. Consider this for a moment: You’d be hard pressed to find an application we build at Epicodus that does not fulfill the lowest common denominator outlined above.

Because this underlying principle always holds, our apps, whether they be in PHP or Ruby, Android or vanilla JS, all have overarching commonalities.

Consider the following chart:

web-vs-android-chart

Introducing Fragments

With that in mind, let's discuss a concept -- Fragments. The good news is that you are already familiar with fragments, at least conceptually!

Fragments are the components of the Android world. They are the equivalent of breaking down code into re-usable pieces. Imagine having to write allll of your code into your Activities - it would quickly get overwhelming and the code would not be modular. Fragments are comprised of both layout code layer, written in XML, and functional code layer, written in Java. They are really very similar to Activities in this sense, just smaller and more modular. The reasons behind breaking code down into fragments are the same as for any JavaScript framework, or MVC components in another language or framework, for example Angular JS components. And the complications that arise, such as communicating with fragments, and retrieving information from fragments, are the same as with components in any other framework. But don’t worry, we will go through this step by step!

Later on, we will work extensively with completely custom fragments, but for now, let’s get familiarized with the intricacies of fragments by working with some stock fragments that are defined in the Android package. We’ll build a simple UI where we utilize one of the most common fragments, a DialogFragment. This will be useful practice for crafting your UIs for your independent and capstone projects. Making UIs can be time consuming in Android - do not underestimate the amount of time it can take to build even a simple interface!

Let's get coding.

Adding a DialogFragment to our App

  1. Create a new project with the settings we have been working with so far.
  2. Name your project and your package whatever you like, and keep the first activity called MainActivity.
  3. Now, define a new class inside of your main package. Name it MoodDialogFragment.
  4. Similarly to how we have already practiced extending adapters and customizing them, make MoodDialogFragment extend DialogFragment

Implement the following Override in MoodDialogFragment:

MoodDialogFragment.java
   @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_mood_dialog, container, false);
        getDialog().setTitle("Simple Dialog");
        return rootView;
    }

After you import the necessary packages, the only thing that Android Studio isn’t happy with is fragment_mood_dialog, which makes sense: We haven’t defined this XML file yet. Let’s do that now.

Create a new XML/Layout XML File and name it fragment_mood_dialog.

Add in the following XML:

fragment_mood_dialog.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:weightSum="1"
   android:orientation="vertical"
   android:id="@+id/baseLayout">


   <TextView
       android:text="How are you feeling today?"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:id="@+id/MoodLabel"
       android:textColor="@color/colorPrimaryDark"
       android:typeface="monospace"
       android:textStyle="normal|bold"
       android:layout_marginTop="10dp"
       android:textAlignment="center"
       android:padding="10dp" />

   <LinearLayout
       android:orientation="horizontal"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:layout_marginTop="50dp">

       <RadioGroup
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:orientation="vertical"
           android:id="@+id/moodRadioGroup"
           android:padding="10dp">

           <RadioButton
               android:text="Great"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:id="@+id/radioButton1"
               android:layout_weight="1" />
           <RadioButton
               android:text="Good"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:id="@+id/radioButton2"
               android:layout_weight="1" />
           <RadioButton
               android:text="Okay"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:checked="true"
               android:id="@+id/radioButton3"
               android:layout_weight="1" />
           <RadioButton
               android:text="Meh"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:id="@+id/radioButton4"
               android:layout_weight="1" />
           <RadioButton
               android:text="Sad"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:id="@+id/radioButton5"
               android:layout_weight="1" />
       </RadioGroup>
   </LinearLayout>
</LinearLayout>

We’re introducing a few new concepts here, but they shouldn’t be entirely unfamiliar.

A RadioGroup has to exist inside a LinearLayout, and it's a layout container that allows you to add radio buttons, and can ensure that no two are checked at the same time! Pretty neat. Also, you can see we used several nested LinearLayouts to keep our layout organized.

If we ran the app now, it should build and run, but we would only see the textview defined in the MainActivity. Why? We may have defined our Fragment and its layout, but we actually need to construct a fragment object somewhere in our executable code. What is our executable code? Well our activity, of course!

Showing our fragment

Jump back to MainActivity:

MainActivity.java

FragmentManager fm = getFragmentManager();
MoodDialogFragment moodDialogFragment = new MoodDialogFragment();
moodDialogFragment.show(fm, "Sample Fragment");

The FragmentManager is responsible for adding, replacing, removing fragments dynamically, it needs to be invoked anytime we are completing any of those actions (which is most of the time.)

dialogfragment-with-radiobuttons.png

If we run our app, we see two things. A.) the dialog is opened as soon as the activity’s onCreate() method runs, which might be appropriate in some circumstances, but isn’t something we want most of the time, and B.) We have no buttons to either submit the form or close the Dialog. We can still close the dialog by clicking outside of the highlighted dialog window, but this is not ideal. Let’s fix the first thing first.

Adding buttons and opening our dialog as a click event

Try and work through the following steps without checking out the result. Consult the cheat sheet if you get stuck.

  1. In your MainActivity, create a new button, and call it moodButton.
  2. Add compile 'com.jakewharton:butterknife:7.0.1' to your build.gradle dependencies.
  3. Bind your views with ButterKnife. (Don’t forget ButterKnife.bind(this) .
  4. Set an onClickListener onto your moodButton. Move the code that opens the dialog into that click event.

Can’t make it work? See the cheat sheet for the full code.

Secret tip: Are you tired of having your button texts show up in uppercase? Change the textAppearance to “AppCompat” or set android:textAllCaps=”false” in your XML. As of January, 2017, a google search for “android lowercase button” yields about 416,000 results...)

Alright, if you run your app, we should now have a DialogFragment that opens on button click. Yay!

Let’s take the next step to add some buttons to our Dialog so we can interact with them. Let’s switch over to our fragment_mood_dialog.xml:

Add:

fragment_mood_dialog.xml

…
<Button
   android:text="Submit"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:id="@+id/submitButton"
   android:layout_weight="1" />

<Button
   android:text="cancel"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:id="@+id/cancelButton"
   android:layout_weight="1" />

…

Before you close the </RadioGroup> tag. Run your app, and you should see your buttons at the bottom of your dialog. Great! Now they need some functionality.

Adding functionality to our cancel button and closing our dialog

Let’s tackle closing the dialog button first.

Because we need to respond to the click on the cancel button inside of the dialog, we need to find a way to connect with that view. Recently, we’ve started using ButterKnife to help us simplify binding our views, and that works great when we are inside an activity. But when we are trying to bind views inside a fragment, ButterKnife can’t help us - we need to do this the old fashioned way. Here’s how.

First, let’s declare some member variables to hold our cancel and submit buttons in our onCreateView():

MoodDialogFragment.java
...
Button cancelButton = (Button) rootView.findViewById(R.id.cancelButton);
Button submitButton = (Button) rootView.findViewById(R.id.submitButton);
...

See that here we are accessing our views through through the variable rootView, as we are not inside an activity, but looking for views currently on top of an activity. The name rootView is just another variable name: What’s important here is that this variable holds the inflated layout and therefore has access to the Views we need to retrieve. Cool!

Now we have those buttons, we can begin setting click listeners:

MoodDialogFragment.java
…
cancelButton.setOnClickListener(new View.OnClickListener() {

   @Override
   public void onClick(View v) {
       dismiss();
   }
});

…

dismiss() is a static method we can simple call anytime we want to close the dialog. Try it out, we should now be able to close the dialog by clicking cancel. Here’s the full code for reference so far.

MoodDialogFragment.java

public class MoodDialogFragment extends DialogFragment{

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

       Button cancelButton = (Button) rootView.findViewById(R.id.cancelButton);
       Button submitButton = (Button) rootView.findViewById(R.id.submitButton);


       cancelButton.setOnClickListener(new View.OnClickListener() {

           @Override
           public void onClick(View v) {
               dismiss();
           }
       });
       return rootView;
   }

}

Great! Now, the logical next step is to be able to retrieve the information from the form. Let’s tackle that next.

Returning values from our form

This part is a bit trickier. We can’t just pull the selected radio button directly, instead we need a multi-step process.

  1. First, we need to pull the entire RadioGroup,
  2. Then, we can use that to retrieve the selected radio button’s id.
  3. Next, we can use that to retrieve the “value” of that selected radio button, and log it out.
  4. Then, we'll want to close our dialog, just like we did when the cancel button received a click. Phew.

OK, let’s get cracking.

Implement the following code:

MoodDialogFragment.java
…
RadioGroup surveyRadioGroup = (RadioGroup) rootView.findViewById(R.id.moodRadioGroup); //pull group


…

And now for the click event:

MoodDialogFragment.java
...
submitButton.setOnClickListener(new View.OnClickListener() {

   @Override
   public void onClick(View v) {
int selectedId = surveyRadioGroup.getCheckedRadioButtonId(); //get selected ID
final RadioButton selectedRadioButton = (RadioButton) rootView.findViewById(selectedId); //get r button val via ID
       Log.d("testing", selectedRadioButton.getText().toString());
       dismiss();
   }
});
...

You may also need to change this line:

 View rootView = inflater.inflate(R.layout.fragment_mood_dialog, container, false);

to

final View rootView = inflater.inflate(R.layout.fragment_mood_dialog, container, false);

Try and get to this point on your own using those code snippets and putting them in the right places. If you get stuck, consult the cheat sheet.

dialog-fragment-working.png

After we select a mood and click the submit button, we should see it logged in the console and have the dialog window close. Cool!

Finishing Up & More on Dialogs

Now you have a blueprint on how to implement one of Android’s most widely used Fragments in your app, as well as getting a handle on some of fragments most fundamental topics. This should put you in a great mood :D

Note: If you are interested on learning how to create DialogFragments that do not require a unique layout, there is a great way to make this process quicker and more flexible. See this further exploration lesson if you are interested in learning more about this topic.

Your MainActivity should look like this to open a dialog on button click:

MainActivity.java
public class MainActivity extends AppCompatActivity {
    @Bind(R.id.moodButton)
    Button moodButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
        ButterKnife.bind(this); //if you just copy paste this code in, this WONT work. Why?


        moodButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                FragmentManager fm = getFragmentManager();
                MoodDialogFragment moodDialogFragment = new MoodDialogFragment();
                moodDialogFragment.show(fm, "Sample Fragment");
            }

        });
    }
}

The full code to retrieve the selected radio button's value should look like this:

MoodDialogFragment.java
public class MoodDialogFragment extends DialogFragment{

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

        Button cancelButton = (Button) rootView.findViewById(R.id.cancelButton);
        Button submitButton = (Button) rootView.findViewById(R.id.submitButton);

        final RadioGroup surveyRadioGroup = (RadioGroup) rootView.findViewById(R.id.moodRadioGroup);

        cancelButton.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                dismiss();
            }
        });

        submitButton.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                int selectedId = surveyRadioGroup.getCheckedRadioButtonId();
                final RadioButton selectedRadioButton = (RadioButton) rootView.findViewById(selectedId);
                Log.d("testing output", selectedRadioButton.getText().toString());
                dismiss();
            }
        });

        return rootView;
    }
}