This post is part 2 of my four part tutorial series on how to create and publish your first Android app. The demo app for this series is an Attendance app and in part 1, we ended with a skeleton project that has material design navigation drawer implemented. In this post, we will add navigation to that navigation drawer so that we can click on an item in the navigation drawer and have it actually take us to the selected screen.
Refactor
If you are following from part 1 which I encourage you to do, then you may want to know that I have renamed the ReportFragment
to ProfileFragment
. Right click the ReportFragment
, select refactor and then select rename. Do the same for fragment_report.xml in the layout folder.
Implementing Navigation in Navigation Drawer
As you remembered, our navigation drawer was implemented with RecyclerView and unlike the humble ListView, the RecyclerView
does not implement OnItemClick
. That means with ListView
, it is easy to handle the event what happens when a row in the list is clicked. That simplicity does bring some challenges such as what happens when you have other clickable items inside a row like buttons. The RecyclerView
solved some of this problems with the concept of a LayoutManager
instead of rows.
There are two ways in which we can handle clicks in the RecylerView
– in the Adapter
class and in the calling Activity or Fragment and we will use both approaches in this tutorial. To handle click for the Navigation Drawer follow this steps. To see a full tutorial about how to handle onItemTouch
for RecylerView
, please consult this blog post.
- Implement a method that opens another Activity: The goal of any navigation system is to take you from point A to point B. In the case of the navigation drawer, you want to go from one screen (
Fragment
or Activity) to another screen. This is a simple method below that accepts the name of the Fragment
we want it to open and all it does is open that Fragment
, copy and paste the code below to the end of your Main Activity.
public void openFragment(final Fragment fragment) {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.container, fragment)
.commit();
}
- Create static names for the Fragments – At the top of your Main Activity, create static names to represent your Fragments like this:
private final static int ATTENDANTS_FRAGMENT = 1;
private final static int EVENTS_FRAGMENT = 2;
private final static int REGISTRATION_FRAGMENT = 3;
private final static int SETTINGS_FRAGMENT = 4;
- Create a switch: As you will see shortly, once an item is clicked in the navigation drawer, the
RecyclerView
returns the position of that item that was clicked and we need a simple switch
statement that calls the openFragment()
method based on the position number that was selected. Copy and paste the method below under your openFragment()
method towards the bottom of your MainActivity
:
private void onTouchDrawer(final int position) {
currentFragment = position;
switch (position) {
case ATTENDANTS_FRAGMENT:
openFragment(new AttendantsList());
break;
case REGISTRATION_FRAGMENT:
openFragment(new RegistrationFragment());
break;
case EVENTS_FRAGMENT:
openFragment(new EventListFragment());
break;
case SETTINGS_FRAGMENT:
startActivity(new Intent(this, PreferenceActivity.class));
default:
return;
}
}
- Create a
GestureDetector
: This object determines what type of touch was received whether it is a swipe up or regular tap. Add the code below towards the end of the onCreate
method in your Main Activity.
final GestureDetector mGestureDetector =
new GestureDetector
(MainActivity.this, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapUp(MotionEvent e) {
return true;
}
});
AddOnItemTouchListener
– This listens for a touch, and when an item is touched, we check with the GestureDetector
to detect what kind of touch this is. We are only interested in the click event, we are not handling the swipe event here, and the GestureDetector
returns true
for click events. If the GetureDetector
returns true
, then we get the position of the item that was clicked and pass it to the switch
statement method to determine what was clicked and open that Fragment
. Here is the method.
mRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
@Override
public boolean onInterceptTouchEvent
(RecyclerView recyclerView, MotionEvent motionEvent) {
View child = recyclerView.findChildViewUnder
(motionEvent.getX(),motionEvent.getY());
if (child != null && mGestureDetector.onTouchEvent(motionEvent)){
Drawer.closeDrawers();
onTouchDrawer(recyclerView.getChildLayoutPosition(child));
return true;
}
return false;
}
@Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {
}
});
- Call the
switch
statement to determine the Fragment
that was clicked on – called from the OnItemTouchedListener
above - Open that
Fragment
– Called from the onTouchDrawer
The updated Main Activity should look like this. You should now be able to navigate to the different Fragment
s listed in the navigation drawer. To verify change the Hello World texts in each of the layouts of the Fragment
to Hello Fragment name or change the background color of each Fragment
layout to a different color so you will get visual feedback. Make sure that you added the Toolbar to the PreferenceActivity
just the way you did for the Main Activity.
Model Classes
Now we need to begin to flesh out the model classes in this app. First, let's start with the Attendant, normally guests for an event do not like to give out too much information just to check in to an event, normally name and email address but I have added other properties like address for completion. The ProfileImagePath
and ProfileImageId
are duplicates. We will delete one later. Here is the Attendant
model class:
public class Attendant {
private String Name;
private String Email;
private String Phone;
private String StreetAddress;
private String City;
private String State;
private String PostalCode;
private String ProfileImagePath;
private int ProfileImageId;
}
Next is the Attendance.java class, just have properties for tracking checkout and check in for each event and Attendant
. Here is the content of that class:
public class Attendance {
private Long CheckInTime;
private Long CheckOutTime;
private Event Event;
private Attendant Attendant;
}
And next is the Event
:
public class Event {
private Long EventDate;
private String Venue;
private String City;
private String SerializedAttendantsList;
private String EventPicturePath;
private String EventPictureId;
private Organizer Organizer;
private List<Attendant> GuestList;
}
Notice the serialized attendants list, we will come back to that when we talk about database. The Organizer
class just extends the Attendant
class for now. For each of the classes, within the class file, click on Code on the Android Studio toolbar, then Generate -> Getter & Setters to generate the getters and setters. Alternatively, you can make all the properties public
instead of private
.
The properties of the model classes represent the information that we want to work with or save regarding each model in our app. For example for each attendant, we have their name, email, image, etc. and we can use that information to represent each attendant like this:
Attendance List Layout and Adapter
Let us go ahead and create the layout file for the list above and create the adapter that manages then. From the work we did on the navigation drawer, I am hoping that it is obvious to you that the above screen above is a List
and we continue to use the RecyclerView
since we want to adhere to modern Android development paradigm as was laid out in the material design guideline. And I hope that you should know that each list needs an adapter. So the standard component for any list is:
Custom layout to layout the rows
An Adapter that passes the dataset to the list
The ListView or the Recyclerview and these three are represented in the image below retrieved from Android developer site
In your res/layout folder, add a file called attendants_list_row.xml and here are the contents of that file:
="1.0"="utf-8"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="wrap_content">
<de.hdodenhof.circleimageview.CircleImageView
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_margin="10dp"
android:layout_centerVertical="true"
android:id="@+id/image_view_attendant_head_shot" />
<LinearLayout
android:id="@+id/linear_layout_name_and_buttons"
android:layout_toRightOf="@+id/image_view_attendant_head_shot"
android:paddingLeft="@dimen/margin_padding_large"
android:layout_alignParentRight="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/linear_layout_attendant_name"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical">
<TextView
android:id="@+id/text_view_attendants_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<TextView
android:id="@+id/text_view_attendants_email"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:id="@+id/linear_layout_checkin_button"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:paddingTop="@dimen/margin_padding_normal"
android:orientation="horizontal">
<ToggleButton
android:id="@+id/togglebutton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textOn="Check in"
android:textOff="Check out"
style="@style/UIButton"
/>
<TextView
android:layout_width="@dimen/margin_padding_normal"
android:layout_height="wrap_content" />
<Button
android:id="@+id/button_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_margin="3dp"
android:text="@string/button_profile"
style="@style/UIButton" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
Update your res/values/color.xml to this:
="1.0"="utf-8"
<resources>
<color name="primary">#F45F5F</color>
<color name="primaryDark">#0288D1</color>
<color name="accent">#FFC107</color>
<color name="lightPrimary">#B3E5FC</color>
<color name="textIcon">#FFFFFF</color>
<color name="primaryText">#212121</color>
<color name="secondaryText">#727272</color>
<color name="divider">#B6B6B6</color>
<color name="activated_color">#E8E8E8</color>
<color name="theme_color">@color/primary</color>
<color name="white_transparent">#D9FFFFFF</color>
<color name="transparent">@android:color/transparent</color>
</resources>
Update your res/values/styles.xml to this:
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/primary</item>
<item name="android:windowActionBar">false</item>
<item name="android:textSize">@dimen/text_size_normal</item>
</style>
<style name="UIButton">
<item name="android:textColor">@color/white_transparent</item>
<item name="android:textStyle">bold</item>
<item name="android:background">@drawable/button_selector</item>
<item name="android:minHeight">@dimen/margin_padding_large</item>
<item name="android:minWidth">@dimen/margin_padding_xxlarge</item>
<item name="android:gravity">center</item>
<item name="android:button">@null</item>
<item name="android:paddingLeft">@dimen/margin_padding_small</item>
<item name="android:paddingRight">@dimen/margin_padding_small</item>
</style>
</resources>
In your Adapters
package, add a Java class file called AttendantsAdapter.java and below is the content of that file:
public class AttendantsAdapter extends RecyclerView.Adapter<AttendantsAdapter.ViewHolder>{
private List<Attendant> mAttendants;
private Context mContext;
View rowView;
public static class ViewHolder extends RecyclerView.ViewHolder{
ImageView attendantHeadshot;
TextView attendantName, attendantEmail;
ToggleButton checkInCheckOutButton;
Button profileButton;
public ViewHolder(View itemView) {
super(itemView);
attendantHeadshot = (ImageView) itemView.findViewById(R.id.image_view_attendant_head_shot);
attendantName = (TextView)itemView.findViewById(R.id.text_view_attendants_name);
attendantEmail = (TextView)itemView.findViewById(R.id.text_view_attendants_email);
checkInCheckOutButton = (ToggleButton)itemView.findViewById(R.id.togglebutton);
profileButton = (Button)itemView.findViewById(R.id.button_profile);
}
}
public AttendantsAdapter(List<Attendant> attendantsList, Context context){
mAttendants = attendantsList;
mContext = context;
}
@Override
public AttendantsAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
rowView = LayoutInflater.from(parent.getContext()).inflate(R.layout.attendants_list_row, parent, false);
ViewHolder viewHolder = new ViewHolder(rowView);
return viewHolder;
}
@Override
public void onBindViewHolder(AttendantsAdapter.ViewHolder holder, int position) {
final Attendant selectedAttendant = mAttendants.get(position);
holder.attendantName.setText(selectedAttendant.getName());
holder.attendantEmail.setText(selectedAttendant.getEmail());
Picasso.with(mContext).load
(selectedAttendant.getProfileImageId()).into(holder.attendantHeadshot);
if (position % 2 == 0){
rowView.setBackgroundColor(mContext.getResources().getColor(R.color.activated_color));
}
holder.checkInCheckOutButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
boolean on = ((ToggleButton) v).isChecked();
if (on){
Toast.makeText(mContext, selectedAttendant.getName() +
" checked in ", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(mContext, selectedAttendant.getName() +
" checked out ", Toast.LENGTH_SHORT).show();
}
}
});
holder.profileButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(mContext, selectedAttendant.getName() +
" profile clicked ", Toast.LENGTH_SHORT).show();
}
});
}
@Override
public int getItemCount() {
return mAttendants.size();
}
}
External Library – Buy it or Build It
You might notice the use of Picasso
library in the adapter above or you might receive an error message if you copy and paste, the reason is that it is an external library. And the debate of whether to build everything from scratch or use an existing implementation where necessary is a common question that developers wrestle with all the time and it is often called the buy it or build it debate. For loading images in the Attendants
list, I have opted to use the Picasso library from Square because this app can be extended such that attendants can register with their Facebook or Google account and we need a reliable library that can handle fetching their profile images from the web and display on the list.
Add Picasso
library to your build.gradle file with this line:
compile 'com.squareup.picasso:picasso:2.5.2'
Add RecyclerView
to the fragment_attendants_list.xml layout file like this:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="okason.com.attendanceapp.Fragments.AttendantsList">
<android.support.v7.widget.RecyclerView
android:id="@+id/attendants_recycler_view"
android:scrollbars="vertical"
android:background="@color/white_transparent"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
Add or update the file dimens.xml in res/values folder to hold common dimensions that we will need. And you can find the content of that file here. Now update the AttendantsListFragment.java like this:
public class AttendantsList extends Fragment {
private View mRootView;
private RecyclerView mRecyclerView;
private RecyclerView.Adapter mAdapter;
private RecyclerView.LayoutManager mLayoutManager;
private List<Attendant> mAttendantsList;
public AttendantsList() {
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mRootView = inflater.inflate(R.layout.fragment_attendants_list, container, false);
mRecyclerView = (RecyclerView) mRootView.findViewById(R.id.attendants_recycler_view);
mRecyclerView.setHasFixedSize(true);
mLayoutManager = new LinearLayoutManager(getActivity());
mRecyclerView.setLayoutManager(mLayoutManager);
mAttendantsList = new ArrayList<Attendant>();
mAdapter = new AttendantsAdapter(mAttendantsList, getActivity());
mRecyclerView.setAdapter(mAdapter);
return mRootView;
}
}
Testing
To test this, you need to add some dummy attendants. First, you need to get any temporary images and add them to your drawables folder and you can name them anything. I named mine headshot_1.jpg, headshot_2.jpg, etc. up to 10 and with that, I now create a temp method like the one below and once you do it, you can now uncomment the line addTestGuestList()
above:
private void addTestGuessList() {
Attendant guest1 = new Attendant();
guest1.setName("Debbie Sam");
guest1.setEmail("deb@email.net");
guest1.setProfileImageId(R.drawable.headshot_1);
mAttendantsList.add(guest1);
Attendant guest2 = new Attendant();
guest2.setName("Keisha Williams");
guest2.setEmail("diva@comcast.com");
guest2.setProfileImageId(R.drawable.headshot_2);
mAttendantsList.add(guest2);
Attendant guest3 = new Attendant();
guest3.setName("Gregg McQuire");
guest3.setEmail("emailing@nobody.com");
guest3.setProfileImageId(R.drawable.headshot_3);
mAttendantsList.add(guest3);
Attendant guest4 = new Attendant();
guest4.setName("Nancy Watson");
guest4.setEmail("nancy@hotmail.com");
guest4.setProfileImageId(R.drawable.headshot_4);
mAttendantsList.add(guest4);
Attendant guest5 = new Attendant();
guest5.setName("Sarah Domingo");
guest5.setEmail("sarah@yahoo.com");
guest5.setProfileImageId(R.drawable.headshot_5);
mAttendantsList.add(guest5);
Attendant guest6 = new Attendant();
guest6.setName("Anthony Lopez");
guest6.setEmail("toney@gmail.com");
guest6.setProfileImageId(R.drawable.headshot_6);
mAttendantsList.add(guest6);
Attendant guest7 = new Attendant();
guest7.setName("Chris VanHorn");
guest7.setEmail("chris@worldmail.com");
guest7.setProfileImageId(R.drawable.headshot_7);
mAttendantsList.add(guest7);
Attendant guest8 = new Attendant();
guest8.setName("Frank Krueger");
guest8.setEmail("frank@ymail.com");
guest8.setProfileImageId(R.drawable.headshot_8);
mAttendantsList.add(guest8);
Attendant guest9 = new Attendant();
guest9.setName("Bella Florentino");
guest9.setEmail("bella@outlook.com");
guest9.setProfileImageId(R.drawable.headshot_9);
mAttendantsList.add(guest9);
Attendant guest10 = new Attendant();
guest10.setName("Donna Simons");
guest10.setEmail("donna@company.com");
guest10.setProfileImageId(R.drawable.headshot_10);
mAttendantsList.add(guest10);
}
Summary
We touched on quite a few concepts here that will take a much longer post to try to explain them one by one. If you have questions, use the comments box to ask. I will write a separate post about data persistence.
If you like this tutorial, please share it with someone who can benefit from it or through your social media. If you want to be notified when I release the next tutorial in the series, please use the opt-in form below to join my mailing list. If you need clarifications, use the comment button below to ask questions. If you have feedback for me on how I can improve my tutorials or what topic you want to learn more about, use the contact form to reach out to me.
The post Create and Publish Your First Android App – Part 2 appeared first on Val Okafor.