Lab 3
Part 1: Recipe Browser
- Download and open the base template project for this lab in Android Studio
- The Project has already set up View Binding (discussed at end of lab 2) in MainActivity
- The project contains a small dataset of recipes and their descriptions in the file
/assets/recipes.json
- Secondly, there is a helper class (
Utils
) which contains a function to load the set of recipes from the json into a List of Recipe data class objects. - The Recipe data class has the following fields:
- id: Int
- name: String
- description: String
activity_main.xml
contains a ConstraintLayout with a LinearLayout with the idlayout_recipelist
.
- The application fills the LinearLayout with buttons corresponding to names of recipes in the function displayRecipes().
- The buttons have a click listener, which calls the function
openRecipeDetails( recipe: Recipe)
- Create a 2nd Activity (New -> Activity -> Empty Activity ), call it "DetailsActivity"
- It's XML layout should have 2 TextViews:
- for the recipe name
- for the recipe description
- You may copy this layout: https://pastebin.com/dM9DHunz , or design according to your preference
- It's XML layout should have 2 TextViews:
- Update the openRecipeDetails() function in MainActivity
- It should create a new intent to launch DetailsActivity
-
Intent(this, DetailsActivity::class.java)
-
- Add the name of the recipe to the intent as an Extra (use intent.putExtra( ) ).
- Launch the 2nd activity with your intent using startActivity()
- It should create a new intent to launch DetailsActivity
- Test your application, you should be able to navigate to the 2nd activity, but right now it does not show the recipe name.
- In onCreate() of DetailsActivity, acquire the recipe name using intent.getStringExtra( .. )
- Update the recipe name TextView so that it displays the value.
Let's also display the recipe description in DetailsActivity. We could pass the description as an Extra from MainActivity again, but instead, we are aiming to keep the data sent in Intent Extras lightweight.
Instead, let's only pass the ID of the recipe in the Intent, and make the 2nd activity handle the loading of the dataset and the correct sub-items in the dataset based on the ID only. In this example the entire dataset was loaded into memory earlier anyway, but imagine the case where you are querying from a DB or Web API.
- Update the openRecipeDetails() so that instead of the recipe name, it uses the recipe's ID
- Pass the ID as an Intent extra to DetailsActivity
- Update DetailsActivity:
- Using the ID, find the right Recipe object in the Recipe List.
- You can use
recipes?.find { it.id == id }
, for example. ( Find is an alias to firstOrNull(), it uses a lambda predicate to find the first matching object in a collection or return null if none is found ).
- You can use
- Now that you have the Recipe object, set the values of the name and description TextViews accordingly.
- Using the ID, find the right Recipe object in the Recipe List.
Part 2: Adding a Rating System to our Recipe browser
Let's add functionality so that user can set a rating for every recipe in DetailsActivity and compare them based on rating in ListActivity.
- Update the DetailsActivity XML UI, adding:
- A RatingBar widget
- A button with the text "Save"
- Add a click handler to the "Save" button, make it call a function named
getRatingAndClose()
and implement it, it should:- Get the value of the rating bar as an integer.
- Create an empty intent object (
val resultIntent = Intent()
), put the recipe ID and rating bar value into the intent Extras. - Finish the activity with a result
-
setResult(Activity.RESULT_OK, resultIntent)
andfinish()
-
Now inside of MainActivity, let's handle the result from DetailsActivity. We will replace startActivity
with a slightly different approach.
- In MainActivity, let's define a resultLauncher which we'll use to launch DetailsActivity and handle its results:
- Add a new class-level variable "resultLauncher":
val resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { onRecipeDetailsResult(result) } }
- Replace the
startActivity(..)
in openRecipeDetails withresultLauncher.launch(intent)
- Implement the function
onRecipeDetailsResult(result: ActivityResult)
, which is called from resultLauncher. It should:- Get the plantId extra and score extra values from the attached Intent data object.
- Then, with the score and recipe ID values, call a function
updateButtonColor(score: Int, recipeId: Int)
(we will define this new function) - We want to update the plant buttons color based on the rating. We need to update our Button creation in addRecipeButtons() so that we can find the right button based on the recipe ID.
- Update
addRecipeButtons
: Specify a tag for each button when creating it, e.g.myButton.tag = recipe.id
- Now, in updateButtonColor(..), you can find the button with:
binding.layout_recipelist.findViewWithTag<Button>(recipeID)
- Update
- Now we can update the color.
- In the Utils class, there is a function
getColorForRating
, it fetches different colour resources based on the Int rating argument (e.g. green for 5, red for 1, etc) - Use the above method to finish
updateButtonColor(..)
, like so:
- In the Utils class, there is a function
button.setBackgroundColor( Utils.getColorForRating(... ))
Part 3: Navigating with Fragments and Navigation Component
Let's recreate the above app using fragments. Instead of starting a new Activity, all of our UI will be inside a single MainActivity. Initially, the activity will contain a fragment that displays the list of recipe titles. When a title inside the fragment is clicked, a navigation controller replaces the list fragment with a details fragment.
https://developer.android.com/guide/navigation
- Fetch another fresh copy of the lab base project, open it as a new project.
- In MainActivity XML, replace the root Layout with a FrameLayout. (in XML view this will appear as FragmentContainerView).
- Inside the FrameLayout, place a NavHostFragment
- You will be asked to point to a Navigation resource, we don't have any yet. Let's create one.
- Inside the FrameLayout, place a NavHostFragment
- Create a Navigation Resource
- Find the "+" Icon in the dialog when adding the NavHostFragment. Or alternatively in project structure:
New -> Android Resource file -> Resource Type: Navigation
- Name:
primary_navigation
( not crucial)
- Find the "+" Icon in the dialog when adding the NavHostFragment. Or alternatively in project structure:
We just created a Navigation Graph, which is a resource that describes navigation-related information for an app. Navigation Graphs consist of Destinations (think of them as UI Views) and Actions, an action triggers a transition from one destination to another. For instance, if you have a Shopping Basket Destination, you might have an action "do_checkout" which navigates to the Payment & Checkout Destination. Each Destination is created as a Fragment.
- Add 2 Destinations to the primary_navigation Navigation
- Click on the "+" icon, and choose "Create new destination", use "Blank Fragment" template
- One called ListFragment, another called DetailsFragment
- This generates new Fragment Kotlin classes and XML layouts for you.
- Update the new fragments' XMLs with similar layouts like you had from part 1 of this lab. The auto-generated Fragments have a FrameLayout as root element, you can convert it to something else by right-clicking in Designer view.
- PlantListFragment -
res/fragment_plant_list.xml
should just have an empty vertical LinearLayout - PlantDetailsFragment -
res/fragment_plant_details.xml
should have 2 textViews to display data about the plant
- PlantListFragment -
- In the Navigation Graph, add an Action by dragging an arrow from listFragment to detailsFragment.
- Set the id of the action to be
action_open_recipe
- Set the id of the action to be
At this stage, you should have something like below:
Part 3.1 Implement ListFragment Kotlin code.
- Open
ListFragment.kt
and override the methodonViewCreated
.- This function gets called after the View has been inflated, here is the recommended place to find and manipulate UI elements in the fragment.
- Note: The pre-generated class has a lot of helpers, including placeholder arguments. You can delete everything related to params/arguments and everything inside the "companion object", to keep your code a bit clearer .
- using
view.findViewById(..)
you can get a reference to your LinearLayout.- Copy your addRecipeButtons() function from 1st part of the lab, and call it from onViewCreated().
- You have to modify it slightly, e.g. instead of
this
for Context, you need to call "requireContext()", since you are working in a Fragment. - You can call getRecipesFromJsonFile() in onCreate(..) of the Fragment.
- Also copy "openRecipeDetails()" from the earlier lab solution, however, this time , openPlantDetails() should use the Nav Controller to invoke a navigation event using our previously defined action:
- findNavController().navigate(R.id.action_open_plant)
- Don't worry about passing data yet.
Now test - when you run the App, the ListFragment should appear within your MainActivity, and clicking on a button should navigate to the details Fragment (still empty right now).
Part 3.2 Implementing DetailsFragment
Let's make DetailsFragment display the selected Recipe. This means we need to pass the selected Recipe ID from one fragment to the other, and then fill the UI with data for that element.
- Make sure the
fragment_details.xml
has TextViews for the name and description, like before. - Let's update openRecipeDetails in ListFragment to also add arguments to the .navigate() method.
- Create a Bundle object, put an Int value in it, with key
chosenRecipeId
- Update the .navigate() call with the same action, this time giving the Bundle as a 2nd argument:
findNavController().navigate(R.id.action_open_recipe, argsBundle)
- Create a Bundle object, put an Int value in it, with key
To access the passed arguments from DetailsFragment, let's look at the example generated code in onCreate
- Every Fragment has an arguments property, from which one can retrieve values (if they're set).
- Update DetailsFragment, using onViewCreated and Arguments, to show the selected recipe data.
- Example, for fetching recipe ID: arguments?.getString("chosenRecipeId")
Note: As you may have noticed the auto-generated Fragment classes create some helpful placeholder code for Fragment parameters, defining their keys as constants (e.g. ARG_PARAM1) and even a static class method (newInstance in companion object) which allows for easy programmatic creation of new instances of the Fragment with the defined parameters). Since we are using Navigation Graph, however, we don't really have an use for the newInstance() method.