Click here to Skip to main content
15,883,901 members
Articles / Mobile Apps / Android

Android Programming By An Example: Creating An Airport Schedule Simulator Application

Rate me:
Please Sign up or sign in to vote.
5.00/5 (21 votes)
1 Aug 2018CPOL29 min read 24.4K   1.4K   30  
In this article, we will discuss about the advanced Android application development based on the example of creating a responsive Airport schedule simulator application.

Introduction

In this article, we will demonstrate how to use Android Studio and Java programming language to create a sample Android application implementing the functionality of the advanced responsive user interface from "scratch". The app discussed in this article will implement the functionality of airport flights schedule simulation. During the development lifecycle, we will implement an Android app's responsive user interface used to render lists of either 'arrivals' and 'departures' flights, as well as provide the functionality for dynamically generating and updating the information about flights in the real-time mode.

We will make a large emphasis on several Java language programming aspects, as well as delve into the number of programming techniques that allow us to deliver an advanced Android app, including the aspects of creating a responsive app's drawer and navigation bar app from the very beginning, delivering our own custom views and layouts such as a custom search view bar with action button, overriding the default functionality of generic app's action bar, maintaining the tabbed layout, rendering recycler views that unlike listviews or gridviews allow to create a custom look for items in the lists of data being rendered by the application, creating various layouts with multiple nested fragments, using bottom navigation view, etc.

Besides the app's interface-specific topics, we will also find out how to create an efficient code written in Java to implement the functionality that generates and manipulates the data contents, as well as how to provide the interaction between the code that manipulates the data and the app's user interface.

Specifically, we will implement the functionality of airport flights schedule simulator that generates a dataset of random flights and manipulates these data by simulating the flights arrival and departure time-line by filtering out flights in the real-time mode, dynamically updating the list of flights being rendered. For that purpose, we will use and discuss such topics as using Android app's background tasks, using timers, etc.

Background

Prerequisites (Before We Begin…)

Before we begin the discussion, let’s spend a moment and take a closer look at what development tools and libraries we particularly need so far to build and run our first Android app.

Since, we’re about to use Java programming language to deploy our first application running Android, we must have Java SE installed. For that purpose, we need to download and install Java Standard Edition – SE platform from http://www.java.com/. In turn, Java SE platform contains all libraries and modules required to build and run the code written in Java on your PC.

After we’ve successfully installed Java SE platform, we also need to properly install an IDE and specific libraries needed to create an Android app project and build the code running our application being deployed. There’s the number of various IDEs, programming languages and libraries, such as either Microsoft Visual Studio / C#.NET Xamarin or Android Studio empowered by Android development community, that can be effectively used to create and deploy Android apps.

In this article, to provide an efficiency of the Android apps development lifecycle, platform compatibility, as well as to slipstream the development process, we will particularly use Android Studio and Java programming language for that purpose.

That’s actually why it’s required and highly recommended to download and install Android Studio (https://developer.android.com/studio/) at the development machine after we’ve installed Java SE platform during the previous configuration step.

As we might have already noticed, Android Studio, being installed, consists of the number of development tools including IDE, Java SDK and NDK libraries, Android system emulators, Gradle/Maven – Java compiler’s “make” utility that makes it easier to compile and link codes written in Java programming language.

In turn, Android Studio’s IDE is an efficient and responsive tool used to easily create and edit Android apps’ resources and Java codes implementing the basic app’s functionality.

Besides an efficient and convenient IDE, Android Studio bundle also includes Java SDK libraries required to develop Android app for various targets (phones, tablets, wearables, Android TV,...). Specifically, Android Studio IDE allows to download and install SDKs for the variety of Android system releases via SDK manager, which is a part of Android Studio, or, optionally, by regularly using native SDK manager from Java SDK distribution.

For compiling and linking an app being created, Android Studio’s bundle also includes Gradle/Maven ‘make’ utility mentioned above. While creating our first Android app project in Android Studio, Gradle component is downloaded and configured to be used along with Android Studio’s IDE. Every time, when we’re building and running an Android app’s project, Gradle utility is performing the compilation- and linking-specific tasks, such as creating an apk-package containing the built Android app, ready to be run on either the emulator or an Android device. During the development lifecycle, since a project has been created and configured, we can use multiple versions of Gradle utility, the way as it was discussed in the project creating sections of this article.

To make it possible to run an app during the debugging development phase, Android Studio also includes an Android device emulator supporting various Android system releases, downloadable via Android Studio’s emulators manager, from Google and Android development community website. Running an app on the emulator is much similar to running it on a target Android device.

In the next section of this article, we will demonstrate how to create our first Android app project in the Android Studio’s environment being installed.

Creating Your First Android App Project

The first thing that we have to do after we’ve successfully met all installation and configuration requirements, discussed above, is to run Android Studio and create a project that will implement our airport flight schedule simulation Android app functionality. To do this, we will use Android Studio main dialog by toggling Start a new Android Studio project option:

Image 1

After this, the Android project creating dialog will appear on screen:

Image 2

Here, in this dialog, we must specify an application name (in this case, it’s `AirportApp`), company domain (for example, `epsilon.com`) to properly configure application package, project location, and, particularly, the package name, which, in our case, is `com.epsilon.airportapp`. After we’ve provided all information needed to create a project, click on Next button located at the bottom of this dialog.

After this step, we must properly select and specify our application’s targeting devices, including the proper form factors (either `phone ` or `tablet `), minimal SDK and its version, as well as Android system release version:

Image 3

After we've successfully selected target device and Android release version, for which the following application will be deployed, we also must select a type of app's activity. An activity is normally a Java-class implementing functionality responsible for app's main window creation, events handling as well as accomplishing other user interaction-specific tasks. In fact, a Java-class extending the generic Activity class, or other derived classes, is the main class for any existing Android apps:

Image 4

In this particular case, we start our first Android app development lifecycle with selecting an empty activity as the main activity for our Airport schedule simulator app. Further, we will customize and enhance the default empty activity to provide functionality needed to perform airport schedule simulation tasks.

The final step in Android app creating phase is to configure an activity-based Java class alias, generate a specific activity layout, as well as to configure app's backward compatibility libraries. To do this, we must proceed with the next configuration dialog:

Image 5

During the final step, we must specify an app's activity-based Java-class name that will correspond to the specific activity layout xml-file name being generated. Also, we must specify whether we want to provide the app's backwards compatibility with the older Android releases.

Since we've configured the app's activity, during the final phase, the specific project is being generated and the Android Studio's IDE main window is opened:

Image 6

In the next section of this article, we'll take a short glance at the Android app's project structure created with Android Studio.

Android App's Project Structure

At this point, let's take a closer look at the app's solution tree located at the upper left corner of the Android Studio's IDE main window opened after the app's project has been created. Normally, the solution tree displays the contents of the project being created that exactly corresponds its directory structure saved to a specific location (for example, 'D:\AirportApp').

AndroidManifest.xml

The folder 'manifests' is the first folder that appears at the top of app's solution tree. It basically includes only one file 'AndroidManifest.xml'. The following file primarily contains all configuration data provided in XML format needed to run the application being created. AndroidManifest.xml file has the following structure, that is exactly the same for all Android apps:

XML
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.epsilon.arthurvratz.airportapp">
  <application
     android:allowBackup="true"
     android:icon="@mipmap/ic_launcher"
     android:label="@string/app_name"
     android:roundIcon="@mipmap/ic_launcher_round"
     android:supportsRtl="true"
     android:theme="@style/AppTheme.NoActionBar">
     <activity android:name=".AirportActivity"
        android:theme="@style/AppTheme.NoActionBar"
        android:windowSoftInputMode="stateHidden"
        android:configChanges="orientation|screenSize|keyboard|keyboardHidden">
        <intent-filter>
           <action android:name="android.intent.action.MAIN" />
           <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
     </activity>
  </application>
</manifest>

The second line of AndroidManifest.xml file contains manifest tag, the attributes of which provide the namespace and app's package name information. It contains also a nested tag application having the number of attributes that define label, text orientation and a pair of icons for the app created. The icons and label, specified by the attributes of application tag are basically displayed in the app's main window. Also, the application tag contains an attribute that defines the default app's theme (for example, android:theme="@style/AppTheme"). Optionally, we might want to modify existing or add more attributes to the application tag, in order to provide a custom look and behavior of the app's main window. For example, we might want to change the value android:theme attribute so that our app will override the default generic and use its own implementation of the app's action bar. For that purpose, we need to change the value of the following tag to android:theme="@style/AppTheme.NoActionBar".

Normally, the application tag has the number of nested tags such as the activity tag, used to provide a set of configuration attributes of the main app's activity. By default, the activity tag has only one attribute that defines the name of the main app's activity (e.g. android:name=".AirportActivity"). To modify the app's main activity configuration parameters, we might have a need to add more attributes to the following tag:

XML
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:configChanges="orientation|screenSize|keyboard|keyboardHidden">

In this particular case, we've added the following configuration attributes to our airport schedule simulator app main activity tag listed above. The first attribute is a duplicate of the attribute we've previously specified in the application tag above. The following attribute is used to specify that there's no default generic app's actionbar in the running app will be displayed. The second attribute android:windowSoftInputMode="stateHidden" is used to specify a soft input method will not be automatically rendered when the app is launched. The last attribute android:configChanges="orientation|screenSize|keyboard|keyboardHidden" provides the list of configuration changes overridden by the app. It means that the following changes will be handled by the app, rather than the Android system. Specifically, the app will handle the screen rotation and render a proper interface layout variation depending on the current screen orientation (e.g., either 'portrait' or 'landscape').

The application tag also has the number of innermost nested tags such as intent-filter, action and category. The action and category intent tags inside intent-filter tag specify the main application entry-point. Particularly, these tags specify that the current '.AirportActivity' is the main app's activity:

XML
<intent-filter>
   <action android:name="android.intent.action.MAIN" />
   <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

Gradle Scripts

Now, let's take a look at the 'Gradle Scripts' sibling located at the bottom of our app's solution tree. The following folder contains all script files needed to configure gradle 'make' utility mentioned in the previous section, including two instances of 'build.gradle' files for either project 'AirportApp' or the 'app' module. The first build.gradle file has the following contents:

Java
// Top-level build file where you can add configuration options 
// common to all sub-projects/modules.

buildscript {
   
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.1.3'
       

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

The following file is the non-XML file containing the basic configuration for Gradle repositories, including its build version (e.g., 'com.android.tools.build:gradle:3.1.3'). During the project configuration, the contents of the following file typically remain unchanged.

However, there's a special interest in the second build.gradle file. The second build.gradle file basically contains the definition of the app's project modules dependencies. For example:

Java
apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.epsilon.arthurvratz.airportapp"
        minSdkVersion 24
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0-alpha3'
    implementation 'com.android.support:support-v4:28.0.0-alpha3'
    implementation 'com.android.support:support-v13:28.0.0-alpha3'
    implementation 'com.android.support:design:28.0.0-alpha3'
    implementation 'com.android.support:recyclerview-v7:28.0.0-alpha3'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation 'org.jetbrains:annotations-java5:15.0'
}

To be able to use Android Support Libraries such as v.4,v.7,v.13 as well as RecyclerView and ConstraintLayout, we must add the following lines to the dependencies section of this file:

Java
implementation 'com.android.support:appcompat-v7:28.0.0-alpha3'
implementation 'com.android.support:support-v4:28.0.0-alpha3'
implementation 'com.android.support:support-v13:28.0.0-alpha3'
implementation 'com.android.support:design:28.0.0-alpha3'
implementation 'com.android.support:recyclerview-v7:28.0.0-alpha3'
implementation 'com.android.support.constraint:constraint-layout:1.1.2'

In turn, both 'gradle-wrapper.properties' and 'local.properties' files are another special interest:

gradle-wrapper.properties

#Thu Jul 26 06:49:16 EEST 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip

local.properties

## This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Thu Jul 26 15:02:12 EEST 2018
sdk.dir=C\:\\AndroidSDK

In these files, we can specify the either gradle utility version or the absolute path to the Android SDK location. To do this, we must modify the following lines of both these files:

distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip
sdk.dir=C\:\\Android\AndroidStudio\SDK

Notice: If you change the version of gradle to > gradle-4.6-all.zip, then you'll also need to disable 'Configure on demand' option in 'File' > 'Settings' > 'Build, Execution, Deployment' > 'Compiler'.

App's Activity And Layout File

After we've exactly conformed to all app's project configuration step, let's take a look at our future app's activity Java implementation file and the main app's layout xml-file. The main app's layout file is located under 'res/layout' folder and has the name of 'activity_airport.xml'. The following file initially contains the 'android.support.constraint.ConstraintLayout' tag, which is the default layout for an empty app.

To modify the main app's layout and add our Android app's interface components such as other inline layouts or controls (i.e., 'views'), we must use the Android Studio's layout designer or manually edit the following layout file:

Image 7

To be able to edit the layout in the Android Studio's designer, you must also modify 'styles.xml' that can be located at 'res/values' folder of the app's project:

XML
<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Base.Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

</resources>

Specifically, you must change the value of 'parent' attributes of the 'style' tag from parent="Theme.AppCompat.Light.DarkActionBar" to parent="Base.Theme.AppCompat.Light.DarkActionBar".

The following layout is the default empty app's layout which will be changed during the app's development lifecycle being discussed. Optionally, we can add changes to the app's layout contents by manually editing the 'activity_airport.xml' layout file:

XML
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:
 android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".AirportActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="19dp"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

Further, we'll provide the detailed guidelines on how to use constraint layouts to build a responsive app's interface in one of the succeeding sections of this article.

The final aspect that we're about to discuss at this point is the app's main activity implementation file 'com.epsilon.airportapp/AirportActivity.java':

Java
package com.epsilon.airportapp;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class AirportActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_airport);
    }
}

The following file is contained within the first 'com.epsilon.airportapp' folder and contains the declaration of the 'AirportActivity' Java-class extending the generic 'AppCompatActivity' class. Initially, the AirportActivity class contains only one overridden method 'OnCreate', implementing the functionality for rendering the app's layout as the main content view for the app being created. The following method implements the invocation of either 'onCreate' method in super-class, or setContentView method that accepts the main app's layout resource-id 'R.layout.activity_airport' and provides the basic rendering functionality for the main app's layout. In the future, we will modify the 'AirportActivity' class and add the required functionality to perform airport's flights schedule simulation.

The App's Main Layout Blueprint

At this point, our primary goal is to create a sketch of the airport schedule simulation app's main layout design. To be more specific, the main app's layout will have the following look:

Image 8

As you can see from the figure above, the entire main airport app's layout consists of an advanced variant of 'SearchView' at the topmost, 'TabLayout', in which two lists of either arrival and departure flight will be rendered. Each tab will render a 'RecyclerView' to display lists of flights, 'BottomNavigationView' that allows to navigate through the list of flights that will take place 'yesterday', 'now' and 'tomorrow'. The 'TabLayout' and 'RecyclerView' are rendered by specific fragment layouts that are displayed after toggling the app's drawers navigation menu items or selecting one of the specific tabs.

The main app's layout is mainly based on the 'DrawerLayout' pattern, which means that the app's drawer will be rendered in case when a user's toggling the action bar button at the upper-left corner of the app's main window. The app's drawer regularly might contain the drawer's header based on 'NavigationView', app's main menu, etc.

Beforehand, let's recall that this is not a standard app's layout generated by the project creation wizard. Further, we will discuss about how to implement the airport app's custom layout programmatically.

Designing the App's Main Layout

Now, we've finally maintained the airport app's main layout blueprint, now it's time to create and edit one or more app's layout files. The first file we're about to modify is 'activity_airport.xml'. Since our airport app is intended to have an app's drawer, we're choosing the 'DrawerLayout' as the main app's layout type:

XML
<?xml version="1.0" encoding="utf-8"?>
<!-- Use DrawerLayout as root container for activity -->
<android.support.v4.widget.DrawerLayout xmlns:
 android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/airport_drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <include layout="@layout/content_frame"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <android.support.design.widget.NavigationView        
        android:id="@+id/airport_navigation_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:fitsSystemWindows="true"
        app:menu="@menu/main_menu"
        app:headerLayout="@layout/nav_header_frame"/>

</android.support.v4.widget.DrawerLayout>

In this case, we use the 'android.support.v4.widget.DrawerLayout' as the root tag in our activity_airport.xml file. After this, we also need to create two nested tags such as either the 'include' tag which will include the another portion of the following layout contained in a separate file 'content_frame.xml', or the 'android.support.design.widget.NavigationView' tag, that declares the airport app's drawer layout. Unfortunately, since the drawer layout is used, we cannot modify the layout shown above by using Android Studio's layout designer, but we can manually edit this layout by using a Android Studio's IDE text editor.

The included fragment of the app's main layout is stored in content frame file and is looks like follows:

XML
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:id="@+id/search_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toTopOf="@+id/airport_fragment_container"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.02"
        android:focusable="true"
        android:focusableInTouchMode="true">

        <requestFocus />

        <android.support.v7.widget.SearchView
            android:id="@+id/searchable"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

    </LinearLayout>

    <FrameLayout
        android:id="@+id/airport_fragment_container"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        app:layout_constraintBottom_toTopOf="@+id/flights_navigation"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/search_bar">

    </FrameLayout>

    <android.support.design.widget.BottomNavigationView
        android:id="@+id/flights_navigation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/airport_fragment_container"
        app:menu="@menu/flights_navigation"
        android:theme="@style/AppTheme"/>
</android.support.constraint.ConstraintLayout>

In this file, we normally use 'android.support.constraint.ConstraintLayout' tag as the root tag for this layout. The following tag has the number of inline tags, including 'LinearLayout', in which 'android.support.v7.widget.SearchView' tag is declared, 'FrameLayout' that actually declares a frame that will be programmatically replaced with a specific fragment rendering 'RecyclerView', displaying a list of flights, 'BottomNavigationView' rendering options for filtering out flights by its time. Since we're using constraint layout as the root for the entire content frame, the all nested views and layouts must be properly constrained. Unlike the previous layout, content frame layout can be successfully edited with Android Studio's layout designer. That's actually why we're having an option whether to edit the specific content frame file or use the layout designer to provide the specific constraints to all views within the following layout.

In this case, the best way to interconnect the views in the content frame is to add specific attributes such as 'app:layout_constraintTop_toBottomOf' to each view-tag as it's shown in the source code above. In this fragment of code, we're adding layout constraint attributes to each view-tag vertically and horizontally starting at the uppermost 'LinearLayout' view-tag, to chain all of them in vertical orientation.

At this point, let's get back to the fragment of code that defines the drawer layout for our app. Another view declared inside of 'DrawerLayout' tag is 'android.support.design.widget.NavigationView'. The following view is basically used to render the app's drawer and its menu as it's shown on the blueprint figure above. The using of the navigation view normally requires that we create another app's drawer layout and specific menu declaring items for the app's drawer menu.

To create these layouts, we basically need to create a sub-folder in the '/res' folder of our project and create the specific menu layout resource file called 'main_menu.xml':

XML
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
    <group android:checkableBehavior="single">
        <item
            android:id="@+id/flights"
            android:icon="@drawable/ic_flight_black_24dp"
            android:title="@string/flights" />
        <item
            android:id="@+id/about"
            android:icon="@drawable/ic_star_black_24dp"
            android:title="@string/about" />
    </group>
</menu>

In this file, we must declare the 'menu' tag and also create the 'group' of items inside of it. In this case, the following layout contains a group of two items for each 'flights' or 'about' menu options, displayed in the app's drawer, below its header.

Another layout file 'nav_header_frame.xml' contains the layout rendered in the app's drawer when toggled by a user:

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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="240dp"
    android:background="@drawable/airport_nav_header"
    android:gravity="bottom"
    android:orientation="vertical"
    android:padding="16dp"
    android:theme="@style/ThemeOverlay.AppCompat.Dark">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="103dp"
        android:layout_height="99dp"
        app:srcCompat="@mipmap/ic_launcher_round" />

    <Space
        android:layout_width="352dp"
        android:layout_height="10dp" />

    <TextView
        android:id="@+id/airport_app_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fontFamily="Verdana"
        android:text="@string/nav_header"
        android:textColor="@android:color/background_light"
        android:textIsSelectable="false"
        android:textSize="30sp" />

    <TextView
        android:id="@+id/airport_app_author"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/airport_app_author" />

</LinearLayout>

To create the app's drawer layout, we will use the specific 'LinearLayout' tag. The linear layout, unlike other layouts such as 'CostraintLayout' allows to position all views in the vertical orientation only, and does not require setting up any constraints between views.

To define the proper drawer's layout, we must place the following view tags to inside the linear layout, as well as to provide the background image the app's drawer header. To do this, we specify the following linear layouts attribute: 'android:background="@drawable/airport_nav_header"'. Normally, our linear layout created will contain the following inline views:

  • 'ImageView' - is used to show the airport app's icon
  • 'Space' - to create a gap between the specific views in the linear layout
  • 'TextView' - to print either airport app's title or author's details

Finally, the app's drawer layout rendered by the 'NavigationView' as well as its blueprint will have the following look:

Image 9

In the next section of this article, we'll find out how to implement the functionality of the main airport app's activity.

Creating Custom SearchView With Action Button

SearchView is the first control of the airport schedule simulator app that appears on the top of main app's window. At this point, let's get back to the fragment of 'content_frame.xml'. The 'android.support.v7.widget.SearchView' tag declaring the search view is located prior to all other views of the following layout file, wrapped up by the 'LinearLayout:

XML
<LinearLayout
    android:id="@+id/search_bar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toTopOf="@+id/airport_fragment_container"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="1.0"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintVertical_bias="0.02"
    android:focusable="true"
    android:focusableInTouchMode="true">

    <requestFocus />

    <android.support.v7.widget.SearchView
        android:id="@+id/searchable"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>

We use the linear layout to ensure that the search view does not gain focus after the app has started.

Since we've declared the 'SearchView' tag within the content frame, our goal, at this point, is to provide the functionality and behavior (e.g., make the search view responsive) by implementing the specific code in Java that will instantiate and handle events of our search view.

As we've probably know, in this project, we will not use the generic search view and the app's bar, but will create our own custom search view that combines the basic functionality of the generic search view and the app's action bar.

To create a custom search view with action button, we need to create a new java-class and name it as 'SearchableWithButtonView' that extends the generic 'View' class:

Java
public class SearchableWithButtonView extends View { 
         // SearchView basic functionality implementation java-code goes here...
}

In this class, we need to implement the following methods. setupSearchableWithButton() is the very first method that we need to implement to provide the specific look and behavior to our custom search view:

Java
public void setupSearchableWithButton() {

    // Set background color of the search view
    ((ViewGroup)m_SearchView.findViewById
                (android.support.v7.appcompat.R.id.search_mag_icon).
            getParent()).setBackgroundColor(Color.parseColor("#ffffff"));

    // Set default custom search of the search view button icon
    // and look of the custom search view
    this.setDefaultSearchIcon(); this.setupIconifiedByDefault();

    // Set default search hint displayed in the search view's edit text view
    m_SearchView.setQueryHint("TYPE HERE...");

    // Set default query text and remove focus from the search view
    m_SearchView.setQuery("", false); getRootView().requestFocus();

    // Instantiate the search view object and
    // set default action button click event listener
    m_SearchView.findViewById(android.support.v7.appcompat.R.id.search_mag_icon).
            setOnClickListener(new SearchableViewListener());

    // Instantiate the search view object
    ViewGroup llSearchView = ((ViewGroup)m_SearchView.findViewById(
            android.support.v7.appcompat.R.id.search_mag_icon).getParent());

    // Instantiate object of the text editable inside the search view
    EditText searchEditText = llSearchView.findViewById(
            android.support.v7.appcompat.R.id.search_src_text);

    // Remove the search view text editable default selection
    searchEditText.setSelected(false);

    // Set text editable click event listener
    searchEditText.setOnClickListener(new SearchableViewListener());

    // Set text editable onTextChange listener
    searchEditText.addTextChangedListener(new SearchableViewListener());
}

In this method, we change the appearance and behavior of the generic search view by modifying the background color, search view button icon, removing default selection and focus from the search view when the app starts, and, also, set handlers (i.e., listeners) of various search view events such as clicking on the search view button that serves as the app's main action button, text editing and text editable view clicking, etc.

Those events handlers are implemented as the 'SearchableWithButtonView' child class, declared inside of it:

Java
public class SearchableViewListener
         implements OnClickListener, TextWatcher {
     @Override
     public void onClick(View view) {

         // Check if the custom search view button was clicked
         if (android.support.v7.appcompat.R.
                 id.search_mag_icon == view.getId()) {

             // If so, perform a check if the default action bar icon was set
             if (!isDefaultIcon) {

                 // If not, set the default icon by invoking setDefaultSearchIcon() method
                 setDefaultSearchIcon();

                 // Terminate the onClick handler method execution
                 return;
             }

             // Invoke onClick(...) method from the main app's activity class
             m_ClickListener.onClick(view);
         }

         // Otherwise, set navigation-back search icon
         else setNavBackSearchIcon();
     }

     @Override
     public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

         // Invoke the beforeTextChange(...) method from app's activity class
         // (e.g. its parent)
         m_TextWatcherListener.beforeTextChanged(charSequence, i, i1, i2);
     }

     @Override
     public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
         // Set navigation-back icon and invoke the onTextChanged(...) method
         // from app's activity class (e.g. its parent)
         setNavBackSearchIcon();
         m_TextWatcherListener.onTextChanged(charSequence, i, i1, i2);
     }

     @Override
     public void afterTextChanged(Editable editable) {

         // Perform a check if the editable string is empty
         if (editable.toString().isEmpty())

             // If so, set default search view icon
             setDefaultSearchIcon();

         // Invoke the afterTextChanged(...) method from its parent
         m_TextWatcherListener.afterTextChanged(editable);
     }
 }

Also the 'SearchableWithButtonView' class has the following methods:

The method listed below changes the look of the custom search view to uniconfied:

Java
   private void setupIconifiedByDefault() {
        // Disable the iconfied mode to make the search view 
        // fill the entire area horizontally
        m_SearchView.setIconified(false);
        m_SearchView.setIconifiedByDefault(false);
    }

The following method replaces the default icon of the generic search view with the custom icon of the app's action bar button:

Java
private void setDefaultSearchIcon() {
    // Replace the default search view icon with the action button icon
    this.isDefaultIcon = true;
    this.replaceSearchIcon(R.drawable.ic_dehaze_white_24dp);
}

The following method replaces the default action bar button icon with the navigation-back icon:

Java
private void setNavBackSearchIcon() {
    // Check if the default icon was set
    if (this.isDefaultIcon == true) {
        // If so, replace search view icon with navigation-back icon
        this.isDefaultIcon = false;
        this.replaceSearchIcon(R.drawable.ic_arrow_back_black_24dp);
        // Run the search view icon animation
        this.setupAnimation();
    }
}

The following method replaces the default search view button icon with an icon retrieved from the app's resources:

Java
private void replaceSearchIcon(int resDefaultIcon) {
    // Instantiate search view button icon object and set the custom icon
    // by calling setImageDrawable method that accepts the icon object retrieved
    // from the app's resources by calling the context's getDrawable(...) method
    ((ImageView)m_SearchView.findViewById
     (android.support.v7.appcompat.R.id.search_mag_icon)).
            setImageDrawable(m_Context.getDrawable(resDefaultIcon));
    // Start animating icon
    this.setupAnimation();
}

This method is used to set up the animation for the search view icon:

Java
private void setupAnimation() {

    // Instantiate search view icon object
    final ImageView searchIconView = m_SearchView.findViewById(
            android.support.v7.appcompat.R.id.search_mag_icon);

    // Compute the icon's width and height values
    int searchIconWidth = searchIconView.getWidth();
    int searchIconHeight = searchIconView.getHeight();

    // Instantiate RotateAnimation class object and specify the rotation params
    RotateAnimation searchIconAnimation = new RotateAnimation(0f, 360f,
            searchIconWidth / 2, searchIconHeight / 2);
    // Set animation interpolator
    searchIconAnimation.setInterpolator(new LinearInterpolator());
    // Set animation repeat count
    searchIconAnimation.setRepeatCount(Animation.INFINITE);
    // Set animation duration
    searchIconAnimation.setDuration(700);

    // Start animating the icon
    searchIconView.startAnimation(searchIconAnimation);

    // Perform a delay for 700ms after the icon animation ends
    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            searchIconView.setAnimation(null);
        }
    }, 700);
}

By using the following method, we override the basic functionality of findViewById(...) method to be used with search view object:

Java
// Override the default findViewById method to be used to Instantiate
// search view object
private SearchView findSearchViewById(int resId) {
    return ((Activity)m_Context).findViewById(resId);
}

By calling these two methods, we set the click event listener and text change event listener used in main app's activity class:

Java
public void setSearchButtonClickListener(@Nullable OnClickListener clickListener) {
    // Set click listener class object of its parent
    m_ClickListener = clickListener;
}
Java
public void setTextWatchListener(@Nullable TextWatcher textWatchListener) {
    // Set text change watcher listener class object of its parent
    m_TextWatcherListener = textWatchListener;
}

Now, since we've implemented the customized search view with action button, it's time to add its functionality to the main app's activity as follows:

Java
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_airport);

    // Instantiating the drawer layout object
    m_DrawerLayout = findViewById(R.id.airport_drawer_layout);
    // Instantiating the navigation view object
    m_navigationView = findViewById(R.id.airport_navigation_view);

    // Instantiating our custom search view object
    m_searchableWithButtonView =
            new SearchableWithButtonView(AirportActivity.this, R.id.searchable);

    // Setting up our custom search view
    m_searchableWithButtonView.setupSearchableWithButton();
    // Adding the text change watcher listener
    m_searchableWithButtonView.setTextWatchListener(new SearchableWithButtonListener());
    // Adding the search view action button click event listener
    m_searchableWithButtonView.setSearchButtonClickListener
                               (new SearchableWithButtonListener());

    // Setup app's drawer menu click event listener
    m_navigationView.setNavigationItemSelectedListener(m_NavigationBarListener);

    // ...

In the overridden onCreate method, we normally perform the instantiation of drawer layout and navigation view objects, setting up our custom search view and add specific events handlers. To handle various search view's events, we must declare a child class 'SearchableWithButtonListener' implementing the either 'View.OnClickListener' or 'TextWatcher' event handling generic classes:

Java
public class SearchableWithButtonListener implements View.OnClickListener, TextWatcher
    {
        @Override
        public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
        }

        @Override
        public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
        }

        @Override
        public void afterTextChanged(Editable editable) {

        }

        @Override
        public void onClick(View view) {
            // Perform a check if the app's drawer open
            if (!m_DrawerLayout.isDrawerOpen(GravityCompat.START))
                // If not, open the app's drawer
                m_DrawerLayout.openDrawer(GravityCompat.START);
        }
    }

The functionality implemented by methods of the following class discussed in one of the next sections of this article. In this case, we will discuss only one implementation of onClick(...) method from this class. The following method implements the app's drawer open functionality by invoking the DrawerLayout.openDrawer(...) method.

As we've already discussed after firing the openDrawer(...) method while the custom action bar click event is handled, the app's drawer is open displaying the app's main menu. At this point, we also must provide the menu items click event handling by calling 'm_navigationView.setNavigationItemSelectedListener(m_NavigationBarListener)' method that accept the object of listener class as its single parameter. The following code implements the overridden navigation menu items click event listener class:

Java
private class NavigationBarListener implements
        NavigationView.OnNavigationItemSelectedListener
{
    // This method handles the navigation menu item click events
    public boolean onNavigationItemSelected(MenuItem menuItem) {
        // set item as selected to persist highlight
        menuItem.setChecked(true);

        //...

        if (m_DrawerLayout.isDrawerOpen(GravityCompat.START))
            m_DrawerLayout.closeDrawers();

        return true;
    }
}

Creating Tabbed App's Layout

As we've already discussed, the airport app is intended to response the user's input and display various content depending on what options from the app's drawer navigation menu or tabs were toggled by a user. Specifically after toggling the 'flights' menu item in the app's drawer navigation menu, it normally renders the tabbed layout. Each tab basically displays a list of flights rendered by the recycler view. To implement this, we will use fragments. A 'Fragment' is a dynamically created and rendered portion of the app's layout containing other layouts or views, or both.

In this case, what we have to do so far is to create specific fragment layouts and our own java-classes implementing the content rendering functionality. As we've already discussed, the two tabs 'arrivals' and 'departures' will appear in the main app's window. In each of these tabs, we will render 'RecyclerView' showing up a list of flights scheduled. To provide a tabbed layout functionality, we will use 'TabbedLayout' rendered inside 'LinearLayout', which is the root layout for the 'FlightsFragment' shown up when a user toggles the first menu item 'flights' in the app's drawer navigation menu. The flights fragment layout is implemented in 'res/layout/ fragment_flights.xml' file:

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

    <android.support.design.widget.TabLayout
        android:id="@+id/flights_destination_tabs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:tabMaxWidth="0dp"
        app:tabMode="fixed"
        app:tabGravity="fill">

    <android.support.design.widget.TabItem
        android:id="@+id/arrivals_tab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:icon="@drawable/ic_flight_land_black_24dp"
        android:text="@string/arrivals_tab" />

    <android.support.design.widget.TabItem
        android:id="@+id/departures_tab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:icon="@drawable/ic_flight_takeoff_black_24dp"
        android:text="@string/departures_tab" />

    </android.support.design.widget.TabLayout>

    <android.support.v4.view.ViewPager
        android:id="@+id/flights_destination_pager"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <requestFocus/>
</LinearLayout>

Inside the flights fragment linear layout, we declare two tags: 'android.support.design.widget.TabLayout' and 'android.support.v4.view.ViewPager'. The first tag basically defines the tabbed layout containing two tabs for either 'arrivals' or 'departures' flights rendering, that appear under the search view in the main app's window. By declaring the second tag 'ViewPager', we provide the functionality for sliding between one entire screen rendering flights to another.

Since the tabbed layout and view pager are rendered as a fragment, we must create a separate java-class 'FlightsFragment' extending the generic 'android.support.v4.app.Fragment' class:

FlightsFragmentImpl.java

Java
package com.epsilon.arthurvratz.airportapp;

import android.net.Uri;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;

import java.util.ArrayList;

public class FlightsFragmentImpl extends android.support.v4.app.Fragment implements
        ArrivalsFragment.OnFragmentInteractionListener,
        DeparturesFragment.OnFragmentInteractionListener
{
        public RecyclerView m_RecyclerView;
        public RecyclerView.Adapter m_RecyclerAdapter;
        public RecyclerView.LayoutManager m_LayoutManager;

        public void setupFlightsRecyclerView
          (RecyclerView recyclerView, ArrayList<AirportDataModel> dataSet)
        {
            // Setting the recycler view object
            m_RecyclerView = recyclerView;

            // Setting the recycler view has a fixed size
            m_RecyclerView.setHasFixedSize(true);

            // Instantiating the linear layout manager object
            m_LayoutManager = new LinearLayoutManager(getContext());

            // Setting up the recycler view's layout manager
            m_RecyclerView.setLayoutManager(m_LayoutManager);

            // Instantiating the flights recycler view's adapter object
            // and adding the flights dataset to the flights recycler view's adapter
            m_RecyclerAdapter = new FlightsRecyclerAdapter(dataSet, getContext());

            // Setting up the flights recycler view's adapter object</span>
            m_RecyclerView.setAdapter(m_RecyclerAdapter);
    }

    @Override
    public void onFragmentInteraction(Uri uri) {

    }
}

FlightsFragment.java

Java
package com.epsilon.arthurvratz.airportapp;

import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class FlightsFragment extends FlightsFragmentImpl
{
    private TabLayout m_TabLayout;
    private ViewPager m_ViewPager;

    final private TabSelectedListener
            m_TabSelListener = new TabSelectedListener();

    public ArrivalsFragment m_ArrivalsFragment;
    public DeparturesFragment m_DeparturesFragment;

    private class TabSelectedListener implements TabLayout.OnTabSelectedListener
    {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
            m_ViewPager.setCurrentItem(tab.getPosition());
        }

        @Override
        public void onTabUnselected(TabLayout.Tab tab) {

        }

        @Override
        public void onTabReselected(TabLayout.Tab tab) {

        }
    }

    private OnFragmentInteractionListener mListener;

    public FlightsFragment() {
        // Required empty public constructor
    }

    public static FlightsFragment newInstance() {
        return new FlightsFragment();
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {

        // Inflating the flights fragment view's object
        View FlightsFragmentView =
                    inflater.inflate(R.layout.fragment_flights, container, false);

        // Instantiating the tab layout object
        m_TabLayout = FlightsFragmentView.findViewById(R.id.flights_destination_tabs);

        // Instantiating view pager object
        m_ViewPager = FlightsFragmentView.findViewById(R.id.flights_destination_pager);

        // Instantiating the tab layout's pager adapter
        FlightsDestPagerAdapter pagerAdapter = new FlightsDestPagerAdapter(
              getChildFragmentManager(), m_TabLayout.getTabCount());

        // Instantiating the arrivals fragment object
        m_ArrivalsFragment = ArrivalsFragment.newInstance();

        // Instantiating the departures fragment object 
        m_DeparturesFragment = DeparturesFragment.newInstance();

        // Adding the arrivals and departure fragment objects to the view pager adapter
        pagerAdapter.add(m_ArrivalsFragment);
        pagerAdapter.add(m_DeparturesFragment);

        // Setting up the view pager adapter
        m_ViewPager.setAdapter(pagerAdapter);

        // Adding the generic page sliding event listener
        m_ViewPager.addOnPageChangeListener(
                new TabLayout.TabLayoutOnPageChangeListener(m_TabLayout));
        m_TabLayout.addOnTabSelectedListener(m_TabSelListener);

        return FlightsFragmentView;
    }

    public void onButtonPressed(Uri uri) {
        if (mListener != null) {
            mListener.onFragmentInteraction(uri);
        }
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (context instanceof OnFragmentInteractionListener) {
            mListener = (OnFragmentInteractionListener) context;
        } else {
            throw new RuntimeException(context.toString()
                    + " must implement OnFragmentInteractionListener");
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mListener = null;
    }

    @Override
    public void onFragmentInteraction(Uri uri) {

    }

    public interface OnFragmentInteractionListener {
        // TODO: Update argument type and name
        void onFragmentInteraction(Uri uri);
    }
}

To implement the flights fragment functionality, we actually define two java-classes. The first class 'FlightsFragmentImpl' extends the generic 'android.support.v4.app.Fragment' and implements the 'OnFragmentInteractionListener' functionality for both 'ArrivalsFragment' and 'DepartureFragment' classes discussed below. The following class implements just one method 'setupFlightsRecyclerView(...)', that accepts two arguments of either a recycler view's object or the dataset 'ArrayList' object discussed later on in this article. The main purpose of this method is to setup the recycler view's adapter that is used to hold the data rendered in the recycler view shown in one of the selected tabs.

Another class 'FlightsFragment' extends the functionality of the 'FlightsFragmentImpl' and provides the basic functionality for dynamically setting up tab layout and view pager in 'OnCreateView' overridden method by adding specific arrivals and departures fragments objects to the view page adapter. The 'FlightsDestPagerAdapter' java-class implements the basic functionality of view pager adapter:

Java
package com.epsilon.arthurvratz.airportapp;

import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;

import java.util.ArrayList;

public class FlightsDestPagerAdapter extends FragmentPagerAdapter {

    private ArrayList<Fragment> m_Fragments = new ArrayList<Fragment>();

    public FlightsDestPagerAdapter(FragmentManager FragmentMgr, int NumberOfTabs) {
        super(FragmentMgr);
    }

    public void add(Fragment fragment)
    {
        m_Fragments.add(fragment);
    }

    @Override
    public Fragment getItem(int position) {
        return m_Fragments.get(position);
    }

    @Override
    public int getCount() {
        return m_Fragments.size();
    }
}

The implementation of the following class is mainly based on using 'ArrayList<Fragment>' functionality used to store an array of generic 'Fragment' class objects.

Finally, to render the flights fragment, we must override the 'onNavigationItemSelected(...)' method in the 'AirportActivity.NavigationBarListener' class. The following method is basically used to handle event from the app's drawer navigation menu and has the following implementation:

Java
private class NavigationBarListener implements
        NavigationView.OnNavigationItemSelectedListener
{
    public boolean onNavigationItemSelected(MenuItem menuItem) {
        // set item as selected to persist highlight
        menuItem.setChecked(true);

        // Instantiate the fragment manager transaction coordinator object
        m_FragmentTran = m_FragmentMgr.beginTransaction();

        // Perform a check if the flights menu item was selected
        if (menuItem.getItemId() == .Rid.flights)

            // If so, replace the airport_fragment_container frame layout
            // with specific flight fragment by using its object.
            m_FragmentTran.replace(R.id.airport_fragment_container,
                FlightsFragment.newInstance());

        else if (menuItem.getItemId() == R.id.about) {}

        m_FragmentTran.addToBackStack(null); m_FragmentTran.commit();

        // Check if the app's drawer is still open
        if (m_DrawerLayout.isDrawerOpen(GravityCompat.START))

            // If so, close the app's drawer
            m_DrawerLayout.closeDrawers();

        return true;
    }

    public void setupInitialFragment()
    {
        if (m_FragmentMgr == null)

            // Instantiate the support fragment manager object
            m_FragmentMgr = getSupportFragmentManager();

            // Begin fragments transaction
            m_FragmentTran = m_FragmentMgr.beginTransaction();

            // Add the default flights fragment object and commit transaction
            m_FragmentTran.add(R.id.airport_fragment_container,
                  FlightsFragment.newInstance()).commit();
    }
}

The following class also implements one more method 'setupInitialFragment(...)' that is used to setup initial fragment when invoked from the app's main activity code, in the overridden method 'OnCreate(...)', when the main app's activity is instantiated:

Java
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_airport);

        //m_ActionToolBar = findViewById(R.id.airport_actionbar);
        m_DrawerLayout = findViewById(R.id.airport_drawer_layout);
        m_navigationView = findViewById(R.id.airport_navigation_view);
        m_flightsNavigationView = findViewById(R.id.flights_navigation);

        //setSupportActionBar(m_ActionToolBar);
        //this.setupActionBar(R.drawable.ic_dehaze_white_24dp);

        m_searchableWithButtonView =
                new SearchableWithButtonView(AirportActivity.this, R.id.searchable);

        m_searchableWithButtonView.setupSearchableWithButton();
        m_searchableWithButtonView.setTextWatchListener(new SearchableWithButtonListener());
        m_searchableWithButtonView.setSearchButtonClickListener
                                      (new SearchableWithButtonListener());

        m_navigationView.setNavigationItemSelectedListener(m_NavigationBarListener);

        m_flightsNavigationView.setSelectedItemId(R.id.flights_now);
        // Setting up initial fragment to be rendered in the main app's window
        m_NavigationBarListener.setupInitialFragment(); this.hideSoftInputKeyboard();

        //...
}

In the next section of this article, we will discuss how to render recycler views inside the flights fragment, showing the lists of either 'arrival' or 'departure' flights.

Rendering Flights In RecyclerView

Rendering lists of flights in the recycler view is the final airport app's GUI topic we're about to discuss in this article. As we already know, our airport application displays two lists of either 'arrival' or 'departure' flights and programmatically does it a similar way. To render lists of flights, all that we have to do is create two fragments that will render either the arrival flights or departure flights recycler views:

fragment_arrivals.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/arrivals_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center_vertical|center_horizontal"
    tools:context=".ArrivalsFragment">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/arrivals_recycler_view"
        android:scrollbars="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</LinearLayout>

fragment_departures.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/departures_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center_vertical|center_horizontal"
    tools:context=".DeparturesFragment">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/departures_recycler_view"
        android:scrollbars="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</LinearLayout>

We also create two java-classes of either 'ArrivalsFragment' and 'DeparturesFragment' that implement those fragments listed above functionality.

ArrivalsFragment.java

Java
package com.epsilon.arthurvratz.airportapp;

import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;

public class ArrivalsFragment extends android.support.v4.app.Fragment {

    public  RecyclerView m_ArrivalsRecyclerView;

    public ArrayList<AirportDataModel> m_ArrivalsDataSet;

    public FlightsFragment m_FlightsFragment;

    private OnFragmentInteractionListener mListener;

    public ArrivalsFragment() {

        // Instantiate the airport app's data model and generate set of random flights
        m_ArrivalsDataSet = new AirportDataModel().InitModel(20);
    }

    public static ArrivalsFragment newInstance() {
        return new ArrivalsFragment();
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        // Inflate the flights fragment layout
        View ArrivalsView =  
             inflater.inflate(R.layout.fragment_arrivals, container, false);

        // Get flights fragment layout object
        m_FlightsFragment =
                (FlightsFragment) this.getParentFragment();

        // Instantiate arrivals recycler view object
        m_ArrivalsRecyclerView =
                ArrivalsView.findViewById(R.id.arrivals_recycler_view);

        // Invoke setupFlightsRecyclerView method, 
        // which is the member of flight fragment class
        m_FlightsFragment.setupFlightsRecyclerView(m_ArrivalsRecyclerView, m_ArrivalsDataSet);

        return ArrivalsView;
    }

    // TODO: Rename method, update argument and hook method into UI event
    public void onButtonPressed(Uri uri) {
        if (mListener != null) {
            mListener.onFragmentInteraction(uri);
        }
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (context instanceof OnFragmentInteractionListener) {
            mListener = (OnFragmentInteractionListener) context;
        } else {
            throw new RuntimeException(context.toString()
                    + " must implement OnFragmentInteractionListener");
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mListener = null;
    }

    public interface OnFragmentInteractionListener {
        // TODO: Update argument type and name
        void onFragmentInteraction(Uri uri);
    }
}

DeparturesFragment.java

Java
package com.epsilon.arthurvratz.airportapp;

import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;

public class DeparturesFragment extends android.support.v4.app.Fragment {

    public  RecyclerView m_DeparturesRecyclerView;

    public ArrayList<AirportDataModel> m_DeparturesDataSet;

    public FlightsFragment m_FlightsFragment;

    private OnFragmentInteractionListener mListener;

    public DeparturesFragment() {
        // Instantiate the airport app's data model and generate set of random flights
        m_DeparturesDataSet = new AirportDataModel().InitModel(20);
    }

    public static DeparturesFragment newInstance() {
        return new DeparturesFragment();
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
	
        // Inflate the flights fragment layout
        View DeparturesView =  
               inflater.inflate(R.layout.fragment_arrivals, container, false);
 
	// Get flights fragment layout object
        m_FlightsFragment =
                (FlightsFragment) this.getParentFragment();

	// Instantiate departures recycler view object
        m_DeparturesRecyclerView =
                DeparturesView.findViewById(R.id.arrivals_recycler_view);

	// Instantiate arrivals recycler view object
        m_FlightsFragment.setupFlightsRecyclerView
               (m_DeparturesRecyclerView, m_DeparturesDataSet);

        return DeparturesView;
    }

    // TODO: Rename method, update argument and hook method into UI event
    public void onButtonPressed(Uri uri) {
        if (mListener != null) {
            mListener.onFragmentInteraction(uri);
        }
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (context instanceof OnFragmentInteractionListener) {
            mListener = (OnFragmentInteractionListener) context;
        } else {
            throw new RuntimeException(context.toString()
                    + " must implement OnFragmentInteractionListener");
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mListener = null;
    }

    public interface OnFragmentInteractionListener {
        // TODO: Update argument type and name
        void onFragmentInteraction(Uri uri);
    }
}

In both these java-classes, we override the functionality of onCreateView(...) method by inflating the flights fragment layout object to invoke the setupFlightsRecyclerView(...) method of 'FlightFragmentImpl' class:

Java
public void setupFlightsRecyclerView
     (RecyclerView recyclerView, ArrayList<AirportDataModel> dataSet)
{
    m_RecyclerView = recyclerView;

    m_RecyclerView.setHasFixedSize(true);

    m_LayoutManager = new LinearLayoutManager(getContext());
    m_RecyclerView.setLayoutManager(m_LayoutManager);

    m_RecyclerAdapter = new FlightsRecyclerAdapter(dataSet, getContext());

    m_RecyclerView.setAdapter(m_RecyclerAdapter);
}

Another important aspect of using recycler views to render lists of flights is the implementation of a recycler view adapter. Since both recycler views for either 'arrival' or 'departure' flights perform the data rendering in a similar way, we just need to implement a single flights recycler view adapter for both specific recycler views.

Also, we must create a layout for each flight item displaying the flight-specific information such as time, destination, airlines code, airlines logo, country flag and status.

flights_item.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/flight_time"
        android:layout_width="51dp"
        android:layout_height="18dp"
        android:layout_marginBottom="8dp"
        android:layout_marginTop="24dp"
        android:text="3:07pm"
        android:textAppearance="@style/TextAppearance.AppCompat.Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/airlines_logo"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.0" />

    <ImageView
        android:id="@+id/airlines_logo"
        android:layout_width="55dp"
        android:layout_height="48dp"
        android:layout_marginBottom="8dp"
        android:layout_marginTop="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/flight_code"
        app:layout_constraintStart_toEndOf="@+id/flight_time"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@color/text_color_secondary" />

    <TextView
        android:id="@+id/flight_code"
        android:layout_width="59dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginTop="24dp"
        android:text="TextView"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/flight_destination"
        app:layout_constraintStart_toEndOf="@+id/airlines_logo"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.0" />

    <TextView
        android:id="@+id/flight_destination"
        android:layout_width="68dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginTop="24dp"
        android:text="TextView"
        android:textAppearance="@style/TextAppearance.AppCompat.Body2"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/country_flag"
        app:layout_constraintStart_toEndOf="@+id/flight_code"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.0" />

    <ImageView
        android:id="@+id/country_flag"
        android:layout_width="51dp"
        android:layout_height="42dp"
        android:layout_marginBottom="8dp"
        android:layout_marginTop="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/flight_status"
        app:layout_constraintStart_toEndOf="@+id/flight_destination"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@android:color/black" />

    <TextView
        android:id="@+id/flight_status"
        android:layout_width="wrap_content"
        android:layout_height="20dp"
        android:layout_marginBottom="8dp"
        android:layout_marginTop="24dp"
        android:text="TextView"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/country_flag"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.0" />

</android.support.constraint.ConstraintLayout>

FlightsRecyclerAdapter.java

Java
package com.epsilon.arthurvratz.airportapp;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.widget.ImageView;
import android.widget.TextView;

import java.text.SimpleDateFormat;
import java.util.ArrayList;

public class FlightsRecyclerAdapter 
  extends RecyclerView.Adapter<FlightsRecyclerAdapter.ViewHolder> {
    // Provide a reference to the views for each data item
    // Complex data items may need more than one view per item, and
    // you provide access to all the views for a data item in a view holder
    private ArrayList<AirportDataModel> m_DataModel;
    private Context m_context;

    public static class ViewHolder extends RecyclerView.ViewHolder {
        // each data item is just a string in this case
        public TextView m_TimeView;
        public TextView m_FlightCodeView;
        public TextView m_DestView;
        public TextView m_StatusView;
        public ImageView m_AirlinesLogoView;
        public ImageView m_CountryFlagView;
        public ViewHolder(View v) {
            super(v);

            // Instantiate each view object in the flights_item layout
            m_TimeView = v.findViewById(R.id.flight_time);
            m_FlightCodeView = v.findViewById(R.id.flight_code);
            m_DestView = v.findViewById(R.id.flight_destination);
            m_StatusView = v.findViewById(R.id.flight_status);

            m_AirlinesLogoView = v.findViewById(R.id.airlines_logo);
            m_CountryFlagView = v.findViewById(R.id.country_flag);
        }
    }

    // Provide a suitable constructor (depends on the kind of dataset)
    public FlightsRecyclerAdapter(ArrayList<AirportDataModel> m_dataModel, Context context) {
        m_DataModel = m_dataModel; m_context = context;
    }

    // Create new views (invoked by the layout manager)
    @Override
    public FlightsRecyclerAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
                                                   int viewType) {
        // create a new view
        View v = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.flights_item, parent, false);

        ViewHolder vh = new ViewHolder(v);
        return vh;
    }

    // Replace the contents of a view (invoked by the layout manager)
    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        // Retrieve specific data for each item rendered in the flights recycler view
        // and display these values in the specific views in the flights_item layout
        holder.m_TimeView.setText(new SimpleDateFormat("HH:mm")
                    .format(m_DataModel.get(position).m_Time));

        holder.m_StatusView.setText(m_DataModel.get(position).m_Status);
        holder.m_DestView.setText(m_DataModel.get(position).m_Destination);
        holder.m_FlightCodeView.setText(m_DataModel.get(position).m_Airlines.m_Flight);

        Context airlines_logo_context = holder.m_AirlinesLogoView.getContext();
        String airlines_logo = m_DataModel.get(position).m_Airlines.m_logoResId;
        holder.m_AirlinesLogoView.setImageResource(this.getResourceIdFromString
                                                  (airlines_logo_context, airlines_logo));

        Context flag_context = holder.m_CountryFlagView.getContext();
        String flag = "flag" + m_DataModel.get(position).m_DestResId;
        holder.m_CountryFlagView.setImageResource
                 (this.getResourceIdFromString(flag_context, flag));

        // Launching animation of each view in the flights_item layout
        setAnimation(holder.m_TimeView, position);
        setAnimation(holder.m_StatusView, position);
        setAnimation(holder.m_DestView, position);
        setAnimation(holder.m_FlightCodeView, position);
        setAnimation(holder.m_AirlinesLogoView, position);
        setAnimation(holder.m_CountryFlagView, position);
}

    public void setAnimation(View view, int pos)
    {
        // Instantinating the animation object
        Animation flightAnimation = android.view.animation.
                AnimationUtils.loadAnimation(m_context, R.anim.fade_interpolator);

        // Set animation duration
        flightAnimation.setDuration(700);

        // Starting the animation
        view.startAnimation(flightAnimation);
    }

    // Return the size of your dataset (invoked by the layout manager)
    @Override
    public int getItemCount() {
        return m_DataModel.size();
    }

    public int getResourceIdFromString(Context context, String resource)
    {
        return context.getResources().getIdentifier(resource,
                "drawable", context.getPackageName());
    }
}

The 'FlightsRecyclerViewAdapter' is the java-class that extends the functionality of the specialization of generic 'RecyclerView.Adapter<FlightsRecyclerAdapter.ViewHolder>'. It implement the basic functionality needed for binding the data to the recycler view. Specifically, it implement a child java-class 'ViewHolder' responsible for rendering each flight item by invoking instantiating the object of each view in flights_item layout. To render specific items, the app is calling onBindViewHolder(...) overridden method to programmatically set specific values to be displayed by the various views inside the flights_item layout.

Adding Flights Schedule Simulation Functionality

As we've already discussed at the very beginning in this article, besides the user interface intended to render the specific flights data, we must implement the functionality responsible for generating the flights data and time-line simulation. Throughout this application, we use the pattern which is something like model-view-controller frequently used in other programming languages and frameworks. Specifically, in this particular case, we're combining our data model with a certain data controller that perform the actual flights dataset manipulation.

AirportDataModel.java

Java
package com.epsilon.arthurvratz.airportapp;

import java.util.ArrayList;
import java.util.Random;

public class AirportDataModel {

    long m_Time;
    String m_Status;
    String m_Destination;
    String m_DestResId;
    Airlines m_Airlines;

    public class Airlines
    {
        public Airlines(String logoResId, String flight)
        {
            this.m_Flight = flight;
            this.m_logoResId = logoResId;
        }

        public String m_logoResId;
        public String m_Flight;
    }

    public AirportDataModel() { }

    public AirportDataModel(long curr_time, String status, String dest, 
                            String destResId, Airlines airlines)
    {
        this.m_Airlines = airlines;
        this.m_Status = status;
        this.m_Destination = dest;
        this.m_Time = curr_time;
        this.m_DestResId = destResId;
    }

    public AirportDataModel getRandomFlight() {

        Random rand_obj = new Random();

        // Instantiate airport flights destination data class object
        AirportFlightsDestData flightsData = new AirportFlightsDestData();

        // Generate random destination city index
        int flight_rnd_index = rand_obj.nextInt(
                flightsData.m_DestCities.size() - 1);

        // Get a string value of a destination city by its random index
        String destCity = flightsData.m_DestCities.get(flight_rnd_index);

        // Get specific country flag resource id associated with the name of the city
        String destResId = flightsData.getFlagResourceByDestCity(destCity);

        // Generate letters in the flight code
        char airline_code_let1 = (char) (rand_obj.nextInt('Z' - 'A') + 'A');
        char airline_code_let2 = (char) (rand_obj.nextInt('Z' - 'A') + 'A');

        String airline_code = "\0";

        // Append letters to the airline_code string value
        airline_code += new StringBuilder().append(airline_code_let1).toString();
        airline_code += new StringBuilder().append(airline_code_let2).toString();

        String flight_code = "\0";

        // Generate four digits of the flight code
        flight_code += new StringBuilder().append(rand_obj.nextInt(9)).toString();
        flight_code += new StringBuilder().append(rand_obj.nextInt(9)).toString();
        flight_code += new StringBuilder().append(rand_obj.nextInt(9)).toString();
        flight_code += new StringBuilder().append(rand_obj.nextInt(9)).toString();

        // Construct a string containing the full airline code

        flight_code = airline_code + " " + flight_code;

        // Instantiate and construct airlines data object
        Airlines airlines = new Airlines(flightsData.m_airlinesResName.get(rand_obj.nextInt(
                flightsData.m_airlinesResName.size() - 1)), flight_code);

        // Get random status string value
        String flight_status = flightsData.m_Status.get(
                rand_obj.nextInt(flightsData.m_Status.size() - 1));

        // Generate a random flight time
        int time_hours_sign = rand_obj.nextInt(2);

        // Generate an hours offset from the current system time
        int time_hours_offset = rand_obj.nextInt(48);

        // Get the current system time
        long currTimeMillis = System.currentTimeMillis();

        // Determine the random flight time in ms
        if (time_hours_sign > 0)
            currTimeMillis += time_hours_offset * 3.6e+6;
        else currTimeMillis -= time_hours_offset * 3.6e+6;

        // Instantiate and return flight item data object based on the
        // data previously generated
        return new AirportDataModel(currTimeMillis,
                flight_status, destCity, destResId, airlines);
    }

    public ArrayList<AirportDataModel> InitModel(int numOfItems)
    {
        // Init model by generated a list of random flight items
        ArrayList<AirportDataModel> newDataModel = new ArrayList<>();
        for (int index = 0; index < numOfItems; index++) {
            newDataModel.add(this.getRandomFlight());
        }

        return newDataModel;
    }

    public ArrayList<AirportDataModel> Simulate(ArrayList<AirportDataModel> dataSet)
    {
        // Get current system time in ms
        long currTimeMillis = System.currentTimeMillis();

        // Get a random current time being simulated
        currTimeMillis += new Random().nextInt(48) * 3.6e+6;

        // Perform a linear search to filter out all flights that already have taken place
        for (int index = 0; index < dataSet.size(); index++) {
            AirportDataModel item = dataSet.get(index);
            if (item.m_Time <= currTimeMillis) {

                // Remove current flight item
                dataSet.remove(item);

                // Generate and add new flight item
                dataSet.add(new Random().nextInt(dataSet.size()), getRandomFlight());
            }
        }

        return dataSet;
    }

    public ArrayList<AirportDataModel> filterByTime(
            ArrayList<AirportDataModel> dataSet, long time_start, long time_end) {
        ArrayList<AirportDataModel> targetDataSet = new ArrayList<>();

        // Perform a linear search to filter out flights which time belongs to a given range
        for (int index = 0; index < dataSet.size(); index++) {
            AirportDataModel item = dataSet.get(index);
            if (item.m_Time > time_start && item.m_Time < time_end)
                targetDataSet.add(item);
        }

        return targetDataSet;
    }
}

In this class, we implement all the methods required to generate and manipulate flights data. The first thing that we need to do is to implement a getRandomFlight(...) method generating a random flight data. The following method basically relies on using a statically declared data. For that purpose, we create another class that also defines and manipulates the flights-specific data.

AirportFlightsDestData.java

Java
package com.epsilon.arthurvratz.airportapp;

import java.util.Arrays;
import java.util.List;

public class AirportFlightsDestData
{
    public class CountryCityRel
    {
        public CountryCityRel(int countryId, int[] cityIds)
        {
            this.m_cityIds = cityIds;
            this.m_countryId = countryId;
        }

        private int m_countryId;
        private int[] m_cityIds;
    }

    public String getFlagResourceByDestCity(String destCity)
    {
        int countryId = -1;

        // Performing a linear search to find the dest city index
        for (int index = 0; index < m_DestCities.size(); index++) {
            if (m_DestCities.get(index) == destCity) {

                // Performing a linear search to find the dest country and return country-id
                for (int country = 0; country < m_CountryCityRelTable.size(); country++) {
                    int[] cityIds = m_CountryCityRelTable.get(country).m_cityIds;
                    for (int city = 0; city < cityIds.length && cityIds != null; city++)
                        countryId = (cityIds[city] == index) ?
                                m_CountryCityRelTable.get(country).m_countryId : countryId;
                }
            }
        }

        return m_countryResName.get(countryId);
    }

    public List<String> m_DestCities = Arrays.asList(
            "Atlanta", "Beijing", "Dubai", "Tokyo", "Los Angeles", "Chicago", 
            "London", "Hong Kong",
            "Shanghai", "Paris", "Amsterdam", "Dallas", "Guangdong", 
            "Frankfurt", "Istanbul", "Delhi", "Tangerang",
            "Changi", "Incheon", "Denver", "New York", "San Francisco", 
            "Madrid", "Las Vegas", "Barcelona", "Mumbai", "Toronto");

    public List<String> m_countryResName = Arrays.asList(
        "peoplesrepublicofchina", "unitedstates", 
        "unitedarabemirates", "japan", "unitedkingdom",
        "hongkong", "france", "netherlands", "germany", "turkey", "india", "indonesia",
        "singapore", "southkorea", "spain", "canada");

    public List<String> m_airlinesResName = Arrays.asList(
            "aa2", "aeromexico", "airberlin", "aircanada", 
            "airfrance2", "airindia2", "airmadagascar",
            "airphillipines", "airtran", 
            "alaskaairlines3", "alitalia", "austrian2", "avianca1",
            "ba2", "brusselsairlines2", 
            "cathaypacific21", "china_airlines", "continental",
            "croatia2", "dagonair", "delta3", "elal2", 
            "emirates_logo2", "ethiopianairlines4",
            "garudaindonesia", "hawaiian2", "iberia2", 
            "icelandair", "jal2", "klm2", "korean",
            "lan2", "lot2", "lufthansa4", "malaysia", 
            "midweat", "newzealand", "nwa1", "oceanic",
            "qantas2", "sabena2", "singaporeairlines", 
            "southafricanairways2", "southwest2",
            "spirit", "srilankan", "swiss", "swissair3", 
            "tap", "tarom", "thai4", "turkish",
            "united", "varig", "vietnamairlines", "virgin4", "wideroe1");

    public List<CountryCityRel> m_CountryCityRelTable =
            Arrays.asList(new CountryCityRel(0, new int[] { 1, 8, 12, }),
                          new CountryCityRel(1, new int[] { 0, 4, 5, 11, 19, 20, 21,23 }),
                          new CountryCityRel(2, new int[] { 2 }),
                          new CountryCityRel(3, new int[] { 3 }),
                          new CountryCityRel(4, new int[] { 6 }),
                          new CountryCityRel(5, new int[] { 7 }),
                          new CountryCityRel(6, new int[] { 9 }),
                          new CountryCityRel(7, new int[] { 10 }),
                          new CountryCityRel(8, new int[] { 13 }),
                          new CountryCityRel(9, new int[] { 14 }),
                          new CountryCityRel(10, new int[] { 15, 22, 25 }),
                          new CountryCityRel(11, new int[] { 16 }),
                          new CountryCityRel(12, new int[] { 17 }),
                          new CountryCityRel(13, new int[] { 18 }),
                          new CountryCityRel(14, new int[] { 21, 24 }),
                          new CountryCityRel(15, new int[] { 26 }));

    public List<String> m_Status = 
           Arrays.asList("Check-In", "Canceled", "Expected", "Delayed");
}

The following class contains a set of generic 'List' objects declared and statically initialized to hold the various data on flights destination cities, as well as the lists with names of resources containing airlines logos and countries flags. Also the following class has the declaration of 'getFlagResourceByDestCity' method used to retrieve data on specific resources names by the name of destination city.

In the airport data model class, we must declare the specific data field variables to hold the data on each flight:

Java
long m_Time;
String m_Status;
String m_Destination;
String m_DestResId;
Airlines m_Airlines;

public class Airlines
{
    public Airlines(String logoResId, String flight)
    {
        this.m_Flight = flight;
        this.m_logoResId = logoResId;
    }

    public String m_logoResId;
    public String m_Flight;
}

Also, in this class, we implement the following methods including getRandomFlight(...), InitModel(...), Simulate(...) and filterByTime(...). As we've already discussed, getRandomFlight method is used to generate a random data for a random flight that later will be added to the list of lights. For that purpose, we invoke the InitModel method in the ArrivalsFragment and DeparturesFragment classes constructor respectively so that each of these constructors will instantiate the airport data model class object, invoke this method and receive its own copy of the array list object containing a list of either arrival or departure flights:

Java
public ArrivalsFragment() {
    m_ArrivalsDataSet = new AirportDataModel().InitModel(20);
}

To provide the list of flights dynamic update during the flights schedule simulation process, we must override the default onResume method for our app's activity class as follows:

Java
@Override
protected void onResume() {
    super.onResume();
    this.findViewById(R.id.search_bar).requestFocus();

    startSimulation();
}

In this method, we're invoking another Simulate(...) method to launch a simulation process:

Java
public void Simulate() {
      simTask = new TimerTask() {
          @Override
          public void run() {
              handler.post(new Runnable() {
                  @Override
                  public void run() {

                     // Instantiate flights fragment object
                     FlightsFragment flightsFragment = (FlightsFragment)
                         m_FragmentMgr.findFragmentById(R.id.airport_fragment_container);

                      m_flightsNavigationView.setSelectedItemId(R.id.flights_now);

                      ArrayList<AirportDataModel> dataSet = null;
                      RecyclerView recyclerView = null;

                      // Determine the currently selected tab
                      TabLayout tabLayout = findViewById(R.id.flights_destination_tabs);

                      if (tabLayout.getTabAt(0).isSelected()) {
                          dataSet = flightsFragment.m_ArrivalsFragment.m_ArrivalsDataSet;
                          recyclerView =
                           flightsFragment.m_ArrivalsFragment.m_ArrivalsRecyclerView;
                      }

                      else if (tabLayout.getTabAt(1).isSelected()) {
                          dataSet = flightsFragment.m_DeparturesFragment.m_DeparturesDataSet;
                          recyclerView =
                            flightsFragment.m_DeparturesFragment.m_DeparturesRecyclerView;
                      }

                      // Invoking airport data model's Simulate method
                      m_AirportDataModel.Simulate(dataSet);

                      // Instantinating the new object for FlightsRecyclerAdapter class and
                      // pass the dataset object as
                      // one of the adapter's constructor parameters

                      FlightsRecyclerAdapter recyclerAdapter
                              = new FlightsRecyclerAdapter(dataSet, getBaseContext());

                      // For the current recycler view object setting the new adapter
                      recyclerView.setAdapter(recyclerAdapter);

                      // Updating the data bound to the new recycler adapter
                      recyclerAdapter.notifyDataSetChanged();
                      recyclerAdapter.notifyItemRangeChanged(0, dataSet.size());
                  }
              });
          }
      };
  }

In this method listed above, we're creating a timer task thread spawned by an instance of timer:

Java
private void startSimulation()
{
    this.Simulate(); new Timer().schedule(simTask, 50, 10000);
}

Every time when the system timer scheduled ticks, the run(...) method is invoked. In the following method, we're invoking the airport data model's Simulate(...) method listed above. The following method determines the system time and filters out all flights items having time value less than the current system time. After that, we create a new instance of recycler view controller previously discussed and pass it the new list of flights as the argument of its constructor. After that, we finally invoke notifyDataSetChange(...) and notifyItemRangeChanged(...) method of the adapter to invalidate the data being update and reflect its changes in the recycler view.

Adding Custom Search View Functionality

As we've already discussed above, our airport app implements the search view rendered at the topmost of the app's main window, to perform an indexed search of flights data by a partial match. At this point, all that we have to do is to add the search functionality to the following custom search view. To do this, we implement onTextChanged method in the main app's activity class searchable with button listener:

Java
   public class SearchableWithButtonListener implements View.OnClickListener, TextWatcher
    {
        @Override
        public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
        }

        @Override
        public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
            // Instantiating the flights fragment object
            FlightsFragment flightsFragment = (FlightsFragment)
                    m_FragmentMgr.findFragmentById(R.id.airport_fragment_container);

            RecyclerView flightsRecyclerView = null;
            ArrayList<AirportDataModel> DataSet, oldDataSet = null;

            // Determining the currently select tab
            TabLayout tabLayout = findViewById(R.id.flights_destination_tabs);

            if (tabLayout.getTabAt(0).isSelected()) {
                // Instantiating the currently active recycler view's object
                flightsRecyclerView = 
                       flightsFragment.m_ArrivalsFragment.m_ArrivalsRecyclerView;

                // Retrieving a list of arrival flights
                oldDataSet = flightsFragment.m_ArrivalsFragment.m_ArrivalsDataSet;
            }

            else if (tabLayout.getTabAt(1).isSelected()) {                
                // Instantiating the currently active recycler view's object
                flightsRecyclerView = 
                   flightsFragment.m_DeparturesFragment.m_DeparturesRecyclerView;

                            // Retrieving a list of departure flights</span>
                oldDataSet = flightsFragment.m_DeparturesFragment.m_DeparturesDataSet;
            }

            // Perform a check if the string is not empty
            if (!charSequence.toString().isEmpty()) {
                // If so instantiate the flights indexed search object 
                // and invoke doSearch method
                // to obtain the list flights which data matches by the partial match
                DataSet = new FlightsIndexedSearch().
                       doSearch(charSequence.toString(), oldDataSet);

                if (DataSet.size() == 0) {
                    DataSet = oldDataSet;
                }
            }

            else DataSet = oldDataSet;

            // Instantiate the new adapter object and 
            // pass the new filtered dataset as argument
            FlightsRecyclerAdapter recyclerAdapter
                   = new FlightsRecyclerAdapter(DataSet, getBaseContext());

            // Setting up the new recycler view's adapter
            flightsRecyclerView.setAdapter(recyclerAdapter);

            // Reflect changes in recycler view
            recyclerAdapter.notifyDataSetChanged();

            recyclerAdapter.notifyItemRangeChanged(0, DataSet.size());
        }

        @Override
        public void afterTextChanged(Editable editable) {

        }

        @Override
        public void onClick(View view) {
            if (!m_DrawerLayout.isDrawerOpen(GravityCompat.START))
                m_DrawerLayout.openDrawer(GravityCompat.START);
        }
    }

When a user types in a text in the search view, the overridden event handling method onTextChanged is invoked. In this method, we're determining the currently active recycler view and execute doSearch method to obtain the list of filtered flight items by a partial match. After that, we're instantiating the new adapter and pass the dataset obtained to the following adapter. Finally, we're invalidating this data in the currently active recycler view. The fragment of code listed below contains the implementation of doSearch method:

Java
package com.epsilon.arthurvratz.airportapp;

import java.util.ArrayList;
import java.util.regex.Pattern;

public class FlightsIndexedSearch {

    public ArrayList<AirportDataModel> doSearch(String text,
               ArrayList<AirportDataModel> dataSet) {

        // Instantinating the empty flights array list object
        ArrayList<AirportDataModel> targetDataset = new ArrayList<>();

        // Performing a linear search to find all flight items which data values
        // match the specific pattern
        for (int index = 0; index < dataSet.size(); index++) {
            AirportDataModel currItem = dataSet.get(index);

            // Applying search pattern to the flight destination string value
            boolean dest = Pattern.compile(".*" + text + ".*",
                    Pattern.CASE_INSENSITIVE).matcher(currItem.m_Destination).matches();

            // Applying search pattern to the airlines flight code string value
            boolean flight = Pattern.compile(".*" + text + ".*",
                    Pattern.CASE_INSENSITIVE).matcher(currItem.m_Airlines.m_Flight).matches();

            // Applying search pattern to the flight status string value
            boolean status = Pattern.compile(".*" + text + ".*",
                    Pattern.CASE_INSENSITIVE).matcher(currItem.m_Status).matches();

            // If one of these values matches the pattern add the current item to the
            // target dataset
            if (dest != false || flight != false || status != false) {
                targetDataset.add(currItem);
            }
        }

        return targetDataset;
    }
}

Adding Bottom Navigation Bar Functionality

The functionality of the bottom navigation bar is much similar to the functionality provided to perform the flights indexed search. To provide this functionality, we must set the navigation item selected listener in the main app's activity class:

Java
m_flightsNavigationView.setOnNavigationItemSelectedListener(
            new BottomNavigationView.OnNavigationItemSelectedListener() {
        @Override
        public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
            FlightsFragment flightsFragment = (FlightsFragment)
                    m_FragmentMgr.findFragmentById(R.id.airport_fragment_container);

            RecyclerView recyclerView = null;
            ArrayList<AirportDataModel> dataSet = null;
            FlightsRecyclerAdapter recyclerAdapter = null;

            // Determining the currently selected tab
            TabLayout tabLayout = findViewById(R.id.flights_destination_tabs);

            if (tabLayout.getTabAt(0).isSelected()) {

                // Getting the currently active recycler view object
                recyclerView = flightsFragment.m_ArrivalsFragment.m_ArrivalsRecyclerView;

                // Getting the dataset of the currently active recycle view
                // (e.g. arrival flights dataset)
                dataSet = flightsFragment.m_ArrivalsFragment.m_ArrivalsDataSet;
            }

            else if (tabLayout.getTabAt(1).isSelected()) {
                // Getting the currently active recycler view object
                recyclerView =
                    flightsFragment.m_DeparturesFragment.m_DeparturesRecyclerView;

                // Getting the dataset of the currently active recycle view
                // (e.g. departure flights dataset
                dataSet = flightsFragment.m_DeparturesFragment.m_DeparturesDataSet;
            }

            // Get current system time value
            long curr_time = System.currentTimeMillis();

            if (menuItem.getItemId() == R.id.flights_prev)
            {
                // Instantinating the new recycler adapter object
                // and pass the filtered list of previous flights items
                // returned by the filterByTime method
                recyclerAdapter = new FlightsRecyclerAdapter
                            (m_AirportDataModel.filterByTime(dataSet,
                            curr_time - (long)3.6e+6 * 48, curr_time), getBaseContext());
            }

            else if (menuItem.getItemId() == R.id.flights_now)
            {
                // Instantinating the new recycler adapter object and
                // pass the filtered list of current flights items
                // returned by the filterByTime method

                recyclerAdapter = new FlightsRecyclerAdapter
                        (m_AirportDataModel.filterByTime(dataSet,
                        curr_time - (long)3.6e+6 * 24, curr_time +
                        (long)3.6e+6 * 24), getBaseContext());

            else if (menuItem.getItemId() == R.id.flights_next)
            {
                // Instantinating the new recycler adapter object and
                // pass the filtered list of next flights items
                // returned by the filterByTime method

                recyclerAdapter = new FlightsRecyclerAdapter
                        (m_AirportDataModel.filterByTime(dataSet,
                        curr_time, curr_time + (long)3.6e+6 * 48), getBaseContext());
            }

            // Setting up the new recycler view's adapter
            recyclerView.setAdapter(recyclerAdapter);

            // Reflect changes in recycler view
            recyclerAdapter.notifyDataSetChanged();
            recyclerAdapter.notifyItemRangeChanged(0, dataSet.size());

            return true;
        }
    });

In this method, we're first determining the currently active recycler view and receive its object. After that, we're performing a check if a user toggled a specific bottom navigation buttons and filtering out all flights that match the given time-line criteria by invoking filterByTime method. Finally, we create a new recycler view adapter and pass the dataset to its constructor, invalidating the currently active recycler view.

Points of Interest

In this article, we've discussed about the several aspects of creating and developing an advanced Android application using various Android and Java programming language techniques including creating custom views and layouts, delivering navigation drawer-based apps, working with fragments and recycler views, implementing custom data adapters and controllers, etc.

History

  • 2nd August, 2018 - The first revision of article was published...

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) EpsilonDev
Ukraine Ukraine
I’m software developer, system analyst and network engineer, with over 20 years experience, graduated from L’viv State Polytechnic University and earned my computer science and information technology master’s degree in January 2004. My professional career began as a financial and accounting software developer in EpsilonDev company, located at L’viv, Ukraine. My favorite programming languages - C/C++, C#.NET, Java, ASP.NET, Node.js/JavaScript, PHP, Perl, Python, SQL, HTML5, etc. While developing applications, I basically use various of IDE’s and development tools, including Microsoft Visual Studio/Code, Eclipse IDE for Linux, IntelliJ/IDEA for writing code in Java. My professional interests basically include data processing and analysis algorithms, artificial intelligence and data mining, system analysis, modern high-performance computing (HPC), development of client-server web-applications using various of libraries, frameworks and tools. I’m also interested in cloud-computing, system security audit, IoT, networking architecture design, hardware engineering, technical writing, etc. Besides of software development, I also admire to write and compose technical articles, walkthroughs and reviews about the new IT- technological trends and industrial content. I published my first article at CodeProject in June 2015.

Comments and Discussions

 
-- There are no messages in this forum --