Lab 3
You can find sample solutions for this lab on the MS Teams channel for lab3
Part 1: Plant Browser
- Download and open the base template project for this lab in Android Studio
- Project uses
minSDK 23
and View Binding in MainActivity - The project contains a small dataset of plants and their descriptions in the file
/assets/plants.json
- Secondly, there is a helper class (
Utils.getPlantsFromJsonFile()
) to load the set of plants from the json into a List of Plant data class objects. - The Plant data class has the following fields:
- id: Int
- name: String
- latinName: String
- description: String
activity_main.xml
contains a ConstraintLayout with a LinearLayout with the idlayout_plantlist
. The application fills the LinearLayout with buttons corresponding to names of plants in the function displayPlants().
- Project uses
- Update the code: for each button, add a click listener, make the click listener call another function
openPlantDetails()
- Create a 2nd Activity (New -> Activity -> Empty Activity ), call it "DetailsActivity"
- It's XML layout should have 2 TextViews:
- for the plant name
- for the plant description
- Design according to your preference
- Example layout: https://pastebin.com/ewj25Ppn
- It's XML layout should have 2 TextViews:
- Update the openPlantDetails() function in MainActivity
- It should create a new intent to launch DetailsActivity
-
Intent(this, DetailsActivity::class.java)
-
- Add the name of the plant to the intent as an Extra (use intent.putExtra( ) ). For this, you should define a new String argument to openPlantDetails()
- 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 plant name.
- In onCreate() of DetailsActivity, acquire the plant name using intent.getStringExtra( .. )
- Update the plant name TextView so that it displays the value.
Let's also display the plant 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 plant 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 openPlantDetails() so that instead of the plant name, it uses the plant's ID
- Pass the ID as an Intent extra to DetailsActivity
- Update DetailsActivity:
- Using the ID, find the right Plant object in the Plant List.
- You can use
plants?.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 Plant object, set the values of the name and description TextViews accordingly.
- Using the ID, find the right Plant object in the Plant List.
Part 2: Adding a Rating System to our Plant browser
Let's add a rating bar - think of it as a quick way to judge a plants health (by visual observation).
- 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:getRatingAndClose( .. )
should:- Get the value of the rating bar as an integer.
- Create an empty intent object (
val resultIntent = Intent()
), put the plant 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. We need to replace "startActivity" with a slightly different approach.
- In MainActivity, declare a property which we'll use to register for Activity results and to launch DetailsActivity:
-
private lateinit var resultLauncher: ActivityResultLauncher<Intent>
-
- Implement a new function "registerForPlantDetailsResult()", call it from onCreate()
- contents should be:
private fun registerForPlantDetailsResult(){ resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { onPlantDetailsResult(result) } } }
- Replace the
startActivity(..)
in openPlantDetails withresultLauncher.launch(intent)
- Create and implement a function
onPlantDetailsResult(result: ActivityResult)
, which is called from registerForPlantDetailsResult(). It should:- Get the plantId extra and score extra values from the attached Intent data object.
- Then, with the score and plant ID values, call a function
updateButtonColor(score: Int, plantId: 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 displayPlants() so that we can find the right button based on the plant ID.
- Specify a tag for each button when creating it, e.g.
myButton.tag = plant.id
- Now, in updateButtonColor(..), you can find the button with:
layout_plant_list.findViewWithTag<Button>(plantId)
- 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
updateButtonColor(..)
, set the color based on the rating/score like so:
- in
// find resource id by using its name val colorId = resources.getIdentifier("color_rating_$score", "color", packageName) button.setBackgroundColor(getColor(colorId))
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 one MainActivity. When created, the activity will first contain a fragment that displays the list of plant 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, and open it as a new project.
- In MainActivity XML, replace the LinearLayout with a NavHostFragment. (in XML view this will appear as FragmentContainerView).
- You will be asked to point to a Navigation resource, we don't have any yet. Let's create one.
- 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 PlantListFragment, another called PlantDetailsFragment
- 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 plantListFragment to plantDetailsFragment.
- Set the id of the action to be
action_open_plant
- Set the id of the action to be
At this stage, you should have something like below:
Part 3.1 Implement PlantListFragment Kotlin code.
- Open
PlantListFragment.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.
- using
view.findViewById(..)
you can get a reference to your LinearLayout. * Copy your displayPlants() 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 "getContext()", since you are working in a Fragment. - You can call getPlantsFromJsonFile() in onCreate(..) of the Fragment.
- Also copy "openPlantDetails()" 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, when you run the App, the PlantListFragment should appear within your MainActivity, and the clicking on a button should navigate to the details (still empty) Fragment.
Part 3.2 Implementing PlantDetailsFragment
Let's make PlantDetailsFragment display the selected Plant. This means we need to pass the selected Plant ID from one fragment to the other, and then fill the UI with data for that element.
- Make sure the
fragment_plant_details.xml
has TextViews for the name and description, like before. - Let's update openPlantDetails in PlantListFragment to also add arguments to the .navigate() method.
- Create a Bundle object, put an Int value in it, with key
chosenPlantId
- Update the .navigate() call with the same action, this time giving the Bundle as a 2nd argument:
findNavController().navigate(R.id.action_open_plant, argsBundle)
- Create a Bundle object, put an Int value in it, with key
To access the passed arguments from PlantDetailsFragment, 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 PlantDetailsFragment, using onViewCreated and Arguments, to show the selected plants data.
- Example, for fetching plant ID: arguments?.getString("chosenPlantId")
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.
Part 4 (Bonus task): Add a Bottom Navigation and Help Fragment
- Add another Destination (Fragment) to your Navigation, called HelpFragment.
- This view will show helpful information to the user how to use this app.
- Make a Global Action to HelpFragment, set its id to
action_open_help
- Add a 2nd Global Action to PlantListFragment, set its id to
action_open_plant_list
- Add a Bottom Navigation Bar to Main Activity.
- Create to menu items for the bar: "Plants" and "Help"
- Make the id-s of these items identical to the 2 actions defined above!
- Create to menu items for the bar: "Plants" and "Help"
- Initialize the Bottom Navigation bar in MainActivity
- Use setupWithNavController(), as described here https://developer.android.com/guide/navigation/navigation-ui#bottom_navigation