Part 1: Recipe browser
- Download and open the base template project for this lab in Android Studio
- The project contains a small dataset of food recipes in the file
/assets/recipes.json
- Secondly, there is a helper class (
Util.getRecipesFromJsonFile()
) 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
- The project contains a small dataset of food recipes in the file
- activity_main.xml contains a LinearLayout with the id
layout_recipelist
. The application fills the LinearLayout with buttons corresponding to names of recipes in the function displayRecipes().
- Update the code: for each button, create a click listener, make the click listener call another function
openDetailsActivity()
(you have to create this new function, you can leave it empty right now)
- Create a 2nd Activity (New -> Activity -> Activity (blank) ), call it "DetailsActivity"
- Its XML layout should have 2 TextViews:
- for the recipe name
- for the recipe description
- Its XML layout should have 2 TextViews:
- Update the openDetailsActivity() 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( ) ). For this, you should define a new String argument to openDetailsActivity()
- 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.
- Update the openDetailsActivity() so that it 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
- Update the DetailsActivy 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.
getRatingAndClose( .. )
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(resultCode, resultIntent)
andfinish()
-
- Now inside of MainActivity, let's handle the resulting rating score from DetailsActivity.
- Replace the
startActivity(..)
withstartActivityForResult(..)
- The request code argument is something you have to choose, it is used to differentiate different activities returning to the current activity (imagine if for each recipe, you could instead of the description, open another activity about their author).
- Override
onActivityResult( .. ):
- Obtain the score value and recipe ID from the attached Intent data object.
- Then, create and call a function @@updateButtonColor(rating: Int, recipeId: Int)
- We want to update the recipe buttons color based on the rating. We need to update our Button creation so that we can find the right button based on the recipe ID.
- Specify a tag for each button when creating it, e.g.
myButton.tag = recipe.id
- Now, in updateButtonColor(..), you can find the button with:
layout_recipe_list.findViewWithTag<Button>(resultId)
- Specify a tag for each button when creating it, e.g.
- Now we can update the color. Let's first add some color resources, update colors.xml to include:
<color name="color_rating_1">#850000</color> <color name="color_rating_2">#F57C00</color> <color name="color_rating_3">#FBC02D</color> <color name="color_rating_4">#AFB42B</color> <color name="color_rating_5">#388E3C</color>
- in onActivityResult, set the color based on the rating/score like so:
// find resource id by using its name val colorId = resources.getIdentifier("color_rating_$score", "color", packageName) button.setBackgroundColor(getColor(colorId))
Part 3: Fragments
Let's recreate the above app using fragments. Instead of starting a new Activity, all of our UI will be inside one MainActivity. When created, the activity will first add a fragment that displays the list of recipe titles. When a title inside the fragment is clicked, the activity then replaces the list fragment with a details fragment within its layout.
- Fetch another fresh copy of the lab base project, and open it as a new project.
- Make the MainActivity XML consist of one FrameLayout with id
fragment_container
. We will place fragments inside of the FrameLayout. - Create a new XML layout file called
fragment_recipelist
. This will be the UI describing the fragment for the recipe list.- It should have a vertical LinearLayout, set the id to
layout_recipelist
- It should have a vertical LinearLayout, set the id to
- Create a new Kotlin class called
RecipeListFragment
and set the contents to be the following:
package com.example.lab3 import android.os.Bundle import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup class RecipeListFragment: Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment val view = inflater.inflate(R.layout.fragment_recipelist, container, false) // TODO: add buttons to the view return view } }
Now, let's add the recipe names to the RecipeListFragment programmatically
- in
onCreateView()
of RecipeListFragment.kt, add buttons showing the recipe titles to the LinearLayout layout_recipelist, similar to the way we did in Part 1 (displayRecipes() )- Note that now you need to refer to all of the XML-defined UI elements through the inflated view object, e.g.
view.layout_recipelist.addView(button)
- Note that now you need to refer to all of the XML-defined UI elements through the inflated view object, e.g.
- Set the button titles, but don't set a click listener yet.
We have programatically designed the look of the RecipeListFragment , but we have not specified for MainActivity to show it yet.
- In MainActivity.kt, create a function called "displayRecipeListFragment":
fun displayRecipeListFragment() { val listFragment = RecipeListFragment() val fragmentManager = supportFragmentManager val transaction = fragmentManager.beginTransaction() transaction .replace(R.id.fragment_container, listFragment, "listFragmentTag") .commit() }
- this function sets RecipeListFragment to be displayed in the
fragment_container
FrameLayout which was defined in MainActivity. It replaces any previous fragment contained in it.
- this function sets RecipeListFragment to be displayed in the
- Now, call the above function inside
onCreate()
of MainActivity - Test the application and make sure MainActivity is displaying ListFragment
DetailsFragment
Now we will create the recipe details fragment. When a list button is clicked, we add it to the fragment_container
with a transcation, similar to above.
- Create another Fragment called RecipeDetailsFragment. The UI XML should have a Layout of your choice, which contains a TextView to show the recipe title and a button to close the Details view.
- Add a 2nd function to MainActivity called "displayDetailsFragment", similar to the displayRecipeListFragment() above.
- It should take as argument the ID of a recipe to be shown
- Add a click handler to the buttons created in RecipeListFragment, so that clicking on them calls the displayDetailsFragment function of the parent MainActivity.
- To access your method defined in MainActivity from a Fragment:
val mainActivity = activity as MainActivity mainActivity.displayDetailsFragment()
- Verify that clicking on an item in ListFragment makes the Activity replace the ListFragment with a new DetailsFragment
- Let's update the displayDetailsFragment() method to pass extra arguments to the fragment (the recipe title)
- You can do this with
fragment.setArguments()
- You can do this with
val arguments = Bundle() arguments.putString("name", title) detailsFragment.arguments = arguments
- In the Fragment onCreateView, you can access those with:
val bundle = this.getArguments() val title = bundle?.getString("name")
- Finally, programmatically add onClickBehaviour to the save button in DetailsFragment XML.
- Don't forget that to get a reference to a XML-defined button inside fragment onCreateView(..) function, you should use
view.my_button_id
not justmy_button_id
!
- Don't forget that to get a reference to a XML-defined button inside fragment onCreateView(..) function, you should use
- Finally, programmatically add onClickBehaviour to the save button in DetailsFragment XML.
- Clicking this button should call displayListFragment()