Build an Android App with Views

1. Before you begin

Introduction

So far, you learned all about building Android apps with Compose. That's a good thing! Compose is a very powerful tool that can simplify the development process. However, Android apps were not always built with declarative UIs. Compose is a very recent tool in the history of Android Apps. Android UIs were originally built with Views. As such, it's highly likely that you will encounter Views as you continue your journey as an Android developer. In this codelab, you learn the basics of how Android apps were built before Compose — with XML, Views, View Bindings, and Fragments.

Prerequisites:

  • Complete the Android Basics with Compose coursework through Unit 7.

What you'll need

  • A computer with internet access and Android Studio
  • A device or emulator
  • The starter code for the Juice Tracker app

What you'll build

In this codelab, you complete the Juice Tracker app. This app lets you keep track of notable juices by building a list consisting of detailed items. You add and modify Fragments and XML to complete the UI and the starter code. Specifically, you build the entry form for creating a new juice, including the UI and any associated logic or navigation. The result is an app with an empty list to which you can add your own juices.

d6dc43171ae62047.png 87b2ca7b49e814cb.png 2d630489477e216e.png

2. Get the starter code

  1. In Android Studio, open the basic-android-kotlin-compose-training-juice-tracker folder.
  2. Open the Juice Tracker app code in Android Studio.

3. Create a Layout

When building an app with Views, you construct the UI inside of a Layout. Layouts are typically declared using XML. These XML layout files are located in the resources directory under res > layout. Layouts contain the components that make up the UI; these components are known as Views. XML syntax consists of tags, elements, and attributes. For more details on XML syntax, reference the Create XML layouts for Android codelab.

In this section, you build an XML layout for the "Type of juice" entry dialog pictured.

87b2ca7b49e814cb.png

  1. Create a new Layout Resource File in the main > res > layout directory called fragment_entry_dialog.

Android studio project pane context pane opened with an option to create a layout resource file.

6adb279d6e74ab13.png

The fragment_entry_dialog.xml layout contains the UI components that the app displays to the user.

Notice that the Root element is a ConstraintLayout. This type of layout is a ViewGroup that lets you position and size Views in a flexible way using the constraints. A ViewGroup is a type of View that contains other Views, called children or child Views. The following steps cover this topic in more detail, but you can learn more about ConstraintLayout in Build a Responsive UI with ConstraintLayout.

  1. After you create the file, define the app name space in the ConstraintLayout.

fragment_entry_dialog.xml

<androidx.constraintlayout.widget.ConstraintLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:layout_width="match_parent"
   android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
  1. Add the following guidelines to the ConstraintLayout.

fragment_entry_dialog.xml

<androidx.constraintlayout.widget.Guideline
   android:id="@+id/guideline_left"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:orientation="vertical"
   app:layout_constraintGuide_begin="16dp" />
<androidx.constraintlayout.widget.Guideline
   android:id="@+id/guideline_middle"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:orientation="vertical"
   app:layout_constraintGuide_percent="0.5" />
<androidx.constraintlayout.widget.Guideline
   android:id="@+id/guideline_top"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:orientation="horizontal"
   app:layout_constraintGuide_begin="16dp" />

These Guidelines serve as padding for other views. The guidelines constrain the "Type of juice" header text.

  1. Create a TextView element. This TextView represents the title of the detail fragment.

110cad4ae809e600.png

  1. Set the TextView an id of header_title.
  2. Set layout_width to 0dp. Layout constraints ultimately define this TextView's width. Therefore, defining a width only adds unnecessary calculations during the drawing of the UI; defining a width of 0dp avoids the extra calculations.
  3. Set the TextView text attribute to @string/juice_type.
  4. Set the textAppearance to @style/TextAppearance.MaterialComponents.Headline5.

fragment_entry_dialog.xml

<TextView
   android:id="@+id/header_title"
   android:layout_width="0dp"
   android:layout_height="wrap_content"
   android:text="@string/juice_type"
   android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5" />

Lastly, you need to define the constraints. Unlike the Guidelines, which use dimensions as constraints, the guidelines themselves constrain this TextView. To achieve this outcome, you can reference the id of the Guideline by which you want to constrain the view.

  1. Constrain the top of the header to the bottom of guideline_top.

fragment_entry_dialog.xml

<TextView
   android:id="@+id/header_title"
   android:layout_width="0dp"
   android:layout_height="wrap_content"
   android:text="@string/juice_type"
   android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
   app:layout_constraintTop_toBottomOf="@+id/guideline_top" />
  1. Constrain the end to the start of guideline_middle and the start to the start of guideline_left to finish the TextView placement. Keep in mind that how you constrain a given view depends entirely on how you want your UI to look.

fragment_entry_dialog.xml

<TextView
   android:id="@+id/header_title"
   android:layout_width="0dp"
   android:layout_height="wrap_content"
   android:text="@string/juice_type"
   android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
   app:layout_constraintTop_toBottomOf="@+id/guideline_top"
   app:layout_constraintEnd_toStartOf="@+id/guideline_middle"
   app:layout_constraintStart_toStartOf="@+id/guideline_left" />

Try to build the rest of the UI based on the screenshots. You can find the completed fragment_entry_dialog.xml file in the solution.

4. Create a Fragment with Views

In Compose, you build layouts declaratively using Kotlin or Java. You can access different "screens" by navigating to different Composables, typically within the same activity. When building an app with Views, a Fragment that hosts the XML layout replaces the concept of a Composable "screen."

In this section, you create a Fragment to host the fragment_entry_dialog layout and provide data to the UI.

  1. In the juicetracker package, create a new class called EntryDialogFragment.
  2. Make the EntryDialogFragment extend the BottomSheetDialogFragment.

EntryDialogFragment.kt

import com.google.android.material.bottomsheet.BottomSheetDialogFragment


class EntryDialogFragment : BottomSheetDialogFragment() {
}

The DialogFragment is a Fragment that displays a floating dialog. BottomSheetDialogFragment inherits from the DialogFragment class, but displays a sheet the width of the screen pinned to the bottom of the screen. This approach matches the design pictured previously.

  1. Rebuild the project, which causes View Binding files based on the fragment_entry_dialog layout to autogenerate. The View Bindings let you access and interact with XML declared Views, you can read more about them in the View Binding documentation.
  2. In the EntryDialogFragment class, implement the onCreateView()function. As the name suggests, this function creates the View for this Fragment.

EntryDialogFragment.kt

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

override fun onCreateView(
   inflater: LayoutInflater,
   container: ViewGroup?,
   savedInstanceState: Bundle?
): View? {
   return super.onCreateView(inflater, container, savedInstanceState)
}

The onCreateView() function returns a View, but right now, it does not return a useful View.

  1. Return the View generated by inflating the FragmentEntryDialogViewBinding instead of returning super.onCreateView().

EntryDialogFragment.kt

import com.example.juicetracker.databinding.FragmentEntryDialogBinding


override fun onCreateView(
   inflater: LayoutInflater,
   container: ViewGroup?,
   savedInstanceState: Bundle?
): View? {
   return FragmentEntryDialogBinding.inflate(inflater, container, false).root
}
  1. Outside of the onCreateView() function, but inside the EntryDialogFragment class, create an instance of the EntryViewModel.
  2. Implement the onViewCreated() function.

After you inflate the View binding, you can access and modify the Views in the layout. The onViewCreated() method is called after onCreateView() in the lifecycle. The onViewCreated() method is the recommended place to access and modify the Views within the layout.

  1. Create an instance of the view binding by calling the bind() method on FragmentEntryDialogBinding.

At this point, your code should look like the following example:

EntryDialogFragment.kt

import androidx.fragment.app.viewModels
import com.example.juicetracker.ui.AppViewModelProvider
import com.example.juicetracker.ui.EntryViewModel

class EntryDialogFragment : BottomSheetDialogFragment() {

   private val entryViewModel by viewModels<EntryViewModel> { AppViewModelProvider.Factory }

   override fun onCreateView(
       inflater: LayoutInflater,
       container: ViewGroup?,
       savedInstanceState: Bundle?
   ): View {
       return FragmentEntryDialogBinding.inflate(inflater, container, false).root
   }

   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val binding = FragmentEntryDialogBinding.bind(view)
    }
}

You can access and set Views through the binding. For example, you can set a TextView through the setText() method.

binding.name.setText("Apple juice")

The entry dialog UI serves as a place for a user to create a new item, but you can also use it to modify an existing item. Therefore, the Fragment needs to retrieve a clicked item. The Navigation Component facilitates navigating to the EntryDialogFragment and retrieving a clicked item.

The EntryDialogFragment is not yet complete, but don't worry! For now, move on to the next section to learn more about using the Navigation Component in an app with Views.

5. Modify the Navigation Component

In this section, you use the navigation component to launch the entry dialog and to retrieve an item, if applicable.

Compose affords the opportunity to render different composables simply by calling them. However, Fragments work differently. The Navigation Component coordinates Fragment "destinations," providing an easy way to move between different Fragments and the Views they contain.

Use the Navigation Component to coordinate navigation to your EntryDialogFragment.

  1. Open the nav_graph.xml file and make sure the Design tab is selected. 783cb5d7ff0ba127.png
  2. Click the 93401bf098936c15.png icon to add a new destination.

d5410c90e408b973.png

  1. Select EntryDialogFragment destination. This action declares the entryDialogFragment in the nav graph, making it accessible for navigation actions.

418feed425072ea4.png

You need to launch the EntryDialogFragment from the TrackerFragment. Therefore, a navigation action needs to accomplish this task.

  1. Drag your cursor over the trackerFragment. Selecting the gray dot and drag the line to the entryDialogFragment. 85decb6fcddec713.png
  2. The nav_graph design view lets you declare arguments for a destination by selecting the destination and clicking the a0d73140a20e4348.png icon next to the Arguments dropdown. Use this feature to add an itemId argument of type Long to the entryDialogFragment; the default value should be 0L.

555cf791f64f62b8.png

840105bd52f300f7.png

Note that the TrackerFragment holds a list of Juice items — if you click one of these items, the EntryDialogFragment launches.

  1. Rebuild the project. The itemId argument is now accessible in the EntryDialogFragment.

6. Complete the Fragment

With the data from the navigation arguments, complete the entry dialog.

  1. Retrieve the navArgs() in the onViewCreated() method of the EntryDialogFragment.
  2. Retrieve the itemId from the navArgs().
  3. Implement the saveButton to save the new/modified juice using the ViewModel.

Recall from the entry dialog UI that the default color value is red. For now, pass this as a place holder.

Pass the item id from the args when calling saveJuice().

EntryDialogFragment.kt

import androidx.navigation.fragment.navArgs
import com.example.juicetracker.data.JuiceColor


class EntryDialogFragment : BottomSheetDialogFragment() {

   //...
   var selectedColor: JuiceColor = JuiceColor.Red

   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val binding = FragmentEntryDialogBinding.bind(view)
        val args: EntryDialogFragmentArgs by navArgs()
        val juiceId = args.itemId

        binding.saveButton.setOnClickListener {
           entryViewModel.saveJuice(
               juiceId,
               binding.name.text.toString(),
               binding.description.text.toString(),
               selectedColor.name,
               binding.ratingBar.rating.toInt()
           )
        }
    }
}
  1. After the data is saved, dismiss the dialog with the dismiss() method.

EntryDialogFragment.kt

class EntryDialogFragment : BottomSheetDialogFragment() {

    //...
    var selectedColor: JuiceColor = JuiceColor.Red
    //...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val binding = FragmentEntryDialogBinding.bind(view)
        val args: EntryDialogFragmentArgs by navArgs()
        binding.saveButton.setOnClickListener {
           entryViewModel.saveJuice(
               juiceId,
               binding.name.text.toString(),
               binding.description.text.toString(),
               selectedColor.name,
               binding.ratingBar.rating.toInt()
           )
           dismiss()
        }
    }
}

Keep in mind that the above code does not complete the EntryDialogFragment. You still need to implement a number of things, such as the population of the fields with existing Juice data (if applicable), the selection of a color from the colorSpinner, the implementation of the cancelButton, and more. However, this code is not unique to Fragments, and you are able to implement this code on your own. Try to implement the rest of the functionality. As a last resort, you can refer to the solution code for this codelab.

7. Launch the entry dialog

The last task is to launch the entry dialog using the Navigation Component. The entry dialog needs to launch when the user clicks the floating action button (FAB). It needs to also launch and pass the corresponding id when the user clicks an item.

  1. In the onClickListener() for the FAB, call navigate() on the nav controller.

TrackerFragment.kt

import androidx.navigation.findNavController


//...

binding.fab.setOnClickListener { fabView ->
   fabView.findNavController().navigate(
   )
}

//...
  1. In the navigate function, pass the action to navigate from the tracker to the entry dialog.

TrackerFragment.kt

//...

binding.fab.setOnClickListener { fabView ->
   fabView.findNavController().navigate(
TrackerFragmentDirections.actionTrackerFragmentToEntryDialogFragment()
   )
}

//...
  1. Repeat this action in the lambda body for the onEdit() method in the JuiceListAdapter, but this time, pass the id of the Juice.

TrackerFragment.kt

//...

onEdit = { drink ->
   findNavController().navigate(
       TrackerFragmentDirections.actionTrackerFragmentToEntryDialogFragment(drink.id)
   )
},

//...

8. Get the solution code

To download the code for the finished codelab, you can use these git commands:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-juice-tracker.git
$ cd basic-android-kotlin-compose-training-juice-tracker
$ git checkout views

Alternatively, you can download the repository as a zip file, unzip it, and open it in Android Studio.

If you want to see the solution code, view it on GitHub.