Android Building Block - Part 2
Android Building Block - Part 2
Android Building Block - Part 2
Yes, although we can create as many Activities as we want, this not the
recommended architecture according to Google. Activity takes the whole
screen, when creating it the system has to create a new context and switch to
it, also it has to create a new window for the Activity’s root view and sometimes
this can take a while especially if we don’t need all of this work.
A long time ago Google introduced a new OS (Android 3.0) just for tablets. The
OS included Fragments as her main key feature. Fragments gave us the ability
to split our whole screen into a bunch of individual units the can work together
and still each one is independent, it has its own Lifecycle events (that
corresponds to the hosting Activity lifecycle). Each Fragment has its own Kotlin
and XML files and the most important thing is that creating it is much quicker
then creating activity since we don’t create a new context or an new window for
its root view but instead we just add its root view to a specific view container in
the Activity layout and it becomes the Fragment host.
Because it was a later addition the Fragments API was added both to the v4
support library (today replaced by the AndroidX) to be used in lower versions of
Android and to the android.app package - based on the idea that when enough
time will pass we will use only the android.app and won’t be needing the
support library anymore. But sometimes realty overcomes and today the
android.app Fragments are deprecated and we should only use the ones from
the AndroidX support library.
Let’s go back to the Google I/O, in 2018 they introduced Navigation as a part of
the Jetpack tools for clean and reliable android apps. The Navigation
component, like the iOS storyboard allows us to create and design in a nice and
easy graphical interface all of our app flow in terms of screens and transitions.
You can create and see in one place all of your app screens and the flow
between them and it’s all done with, what else, Fragments! In fact the only work
the Activity is doing is hosting the Fragments and sometimes interacting with
app menus.
Fragments
In it create a new XML file and add a Floating Action Button in the buttom - end
of the parent. Your xml should look like this:
If you’re there already, create our second screen, it will use for adding an item
that will be shown later on in a list in that our screen so just add the item input
fields in the next screen. Each Item will have a title, a description and an image.
So go ahead and create your UI, don’t forget the finish button. This is the
general layout of the xml file.
Before we need to create our Fragments, let’s understand it’s lifecycle:
The lifecycle of the Activity in which the fragment resides directly affects the
lifecycle of the Fragment. Each lifecycle callback of the activity results in a
similar callback for each hosted Fragment. For example, when the activity
receives onPause(), each fragment in the activity receives onPause().
Fragments have a few extra lifecycle callbacks that handle unique interaction
with the activity in order to perform actions such as build and destroy the
fragment's UI. These additional callback methods are:
onAttach() - Called when the fragment has been associated with the activity
(the Activity is passed in here by the OS). If the Fragment needs the Context
after this function he can retrieve it using the getActivity or requiredActivity
functions.
onCreateView() -Called to create the view hierarchy associated with the
fragment.
onViewCreated() - Called immediately after onCreateView. This gives
subclasses a chance to initialize themselves once they know their view
hierarchy has been completely created. The views aren’t attached to their
parents yet.
onActivityCreated() -Called when the activity's onCreate() method has
returned.
onDestroyView() - Called when the view hierarchy associated with the fragment
is being removed.
onDetach() - Called when the fragment is being disassociated from the activity.
the getActivity() function here will returned null.
Once the activity reaches the resumed state, you can freely add and remove
fragments to the activity. Thus, only while the activity is in the resumed state
can the lifecycle of a fragment change independently.
More than that the Fragment's Views has a separate Lifecycle that is
managed independently from that of the fragment's Lifecycle.
The fragment views can be destroyed while the fragment itself is alive in the
back stack. The back stack designed to imitate the back pressed activity action
for fragments - meaning when the user presses the back button the last
performed action is popped out. If the action included replacing Fragment A
with B then pressing the back button will pop it out and Fragment B will be
replaced with A that waited in back stack to be popped out (the instance
remained alive while the views weren’t). This is important and has affects on
the view bidding as we will see soon.
Fragments and the Fragment Manager
After the onCreate() event the fragment is added to the FragmentManager.
The FragmentManager is responsible of attaching fragments to their hosting
activity and detaching them. When these events happen the fragments
onAttach() and onDetach() are called. After onAttach you can call the
FragmentManager’s findFragmentById() function and get the desired
fragment. Besides managing all or our fragments and giving as the ability to
add, remove, replace and retrieve them, the FragmentManger also manages the
back stack we have talked about before.
Like the Activity the Fragment has its own lifecycle and it implements the
Jetpack’s LifecycleOwner interface that allows to retrieve his lifecycle events
using the getLifecycle() method. This function return a Lifecycle object with the
following states:
● INITIALIZED
● CREATED
● STARTED
● RESUMED
● DESTROYED
But don’t forget that the fragments keeps a separate lifecycle object for its
views in case we need to preform UI related tasks such as start observing data
that will only be shown in a list.
Here are the fragment lifecycle events and its view lifecycle events with their
corresponding callbacks:
We will see more on those Lifecycle states later on when we dive deeper into
Jetpack.
For further reading on the fragment lifecycle please refer to:
https://developer.android.com/guide/fragments/lifecycle
Note that when we inflated the Activity layout we didn’t supply any parent
because the system create a new window just for it, so it’s not joining any
parent. But here we specify a container since we add its root view to a specific
container resides in the hosting activity.
Like said before the Fragment Manager is responsible for exchanging and
managing Fragments. Each transition can include adding, removing or replacing
fragments and is called Fragment transaction. In order to imitate the back
button press for fragment as it is with Activity (remove the last added screen) a
special back stack is created and you can add the Transaction to it. When the
user press the back button the last transaction is removed. The fragment can
live in the back stack although it’s views are destroyed like we said.
All of this work used to be done manually but as of Google I/O 2018 we can do
all of this with the Navigation component.
First we need to add the Navigation graph to our resources and the fragments
and their transitions to it. By looking at the graph we will see all of our app
screens and the flow between them. We can design our app flow in a very nice
and friendly GUI interface and we can even add animations.
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:defaultNavHost="true"
app:navGraph="@navigation/my_nav" />
The defaultNavHost property tells the system to pass the back clicks to this
NavHost so he can pop his back stack.
Optional you can use the “tag” attribute if you want later to reference him using
the Fragment Manager findFragmentByTag().
So now all we have to do is execute the action and pass extra information with
some of them.
To perform these actions we need to get a reference to our Navigation
controller.
We can do this with any child view in the its view hierarchy to by:
Navigation.findNavController(v).navigate([Action id])
Where v is any view in a view tree that its root is the Navigation Controller,
meaning any view from the displayed Fragments.
If you do not have a live view you can also pass the context and a view id:
Navigation.findNavController(this,R.id.text_view).navigate([Action id])
Back stack
Now run the app navigate to the add item screen, press the finish button and
go back to the Home Screen. So far so good.
But press the back button. Strange ah? Not so much. By default each
action(which is a transaction) is added to the back stack, pressing the back
button pop the last action.
You can solve this by pop the back stack with the action. Choose the action In
the navigation and in the pop behavior choose the root navigation container.
This mean that when executing the action all the fragments in the back stack
will get pop up to the very root of the navigation.
Or alternatively you can pop to the home screen but if you do so check the
inclusive check box to also pop the former instance of it from thee (otherwise
you will have to home screens)
Let’s pass the item details (not an object and without the photo yet) and show
them in a Toast message, for now. Next stage we will create a dynamic list in
the all items screen and add the object to it.
Let’s create a bundle with the details and call the navigate function with it as a
parameter.
In the newly created fragment that has this bundle in his arguments property
we get the data
Next design your cell’s layout. With RecyclerView we use CardView. So create a
new xml with the CardView as the root and design your layout:
To achieve this layout add contentPadding and cornerRadius while setting the
cardUseCompatPadding to true in your CardView attributes. This will make
nice separation between the cards. Inside the card add an Horizontal linear
layout with and image and a vertical linear layout with two TextViews. Don’t
forget to give each view an id.
Note the the photo property is nullable since not all items will have a photo(at
least not in the beginning)
But First let’s understand the concept of the Adapter. According to the MVC
design pattern the the controller is a mediator unit between the views and the
model in order to separate between the logic and the UI. The MVA (Model View
Adapter) is very similar.
This approach might appear excessively strict, but has some advantages: the
communication network is artificially constrained, making it easier to evaluate
and debug. All action happens in the Adapter, and the View can be created
from off-the-shelf widgets without any Model-specific variation which make
him more generic.
It responds to the Recycler View requests. First of all the Recycler gets the
amount of items by calling the getItemCount() function and if the amount is
bigger than zero, it asks for them one by one from its Adapter. Now, notice
there are two functions for this: the create function and the bind function. As I
explained before the first cells displayed on the screen (+one more) needs to
be created from scratch, so for them the recycler calls both
onCreateViewHolder supplying himself as the parent and a type (like we said
this is used in case where one recycler cells has more then one layout file), the
function returns the newly created ViewHolder, and with it, the recycler calls
the onBindViewHolder passing the already created empty view holder and the
relevant position, and this function use view holder’s bind function with the
specific Item at the requested position and return cell with the relevant data so
he can Add it to the list and show the user. But as we said before when the user
start scrolling the scrolled out cell moves to the recycle bin and then the
recycler doesn’t have to call the “expensive” onCreateViewHolder but only the
“cheap” and fast onBindViewHolder function. So it’s the recycler choice when
to create the cell or just bind the data according to what it has in its recycle bin.
Later we will get back to it and update the image as well as handling view
events, but for now it’s enough.
Connecting the Recycler to the Adapter and setting the Layout Manager
What we need to do now is to connect between the Recycler and the Adapter
but before that we need to set a Layout Manager to the Recycler View.
A Layout manger decides how the cell will be organized. We have three options:
1. LinearLayoutManager - organizes the cells one after the other like a
scrolling list, it can be either horizontal or vertical.
2. GridLayoutManager - organizes the cells in a grid or a table where
we must supply number of columns.
3. StaggeredGridLayoutManager - is the same as before but each
square in the grid can have a different height (like the notes app)
Before we run the app and see how the magic happens we first need to add
items to the list. So add the new item to the ItemManager object and remove
the bundle from the navigate action. Your code should look like this:
Run your app and test your recycler. It’s working very nicely but be aware of the
fact that the list not saved to the file system.
We want to create exactly this. We want to separate the event from the event
handling, and let another class decide for itself on how to respond to the event
we are reporting about.
So first decide on the events you want to report about and the info you want to
send with it and create your interface inside the Adapter:
Make the Adapter’s constructor to receive an instance of that Listener (as well
as the list) and make the View Holder class (the one receiving this actual
events) to be an inner class so it can access this callback and invoke its
functions (the functions that will be later implemented in the fragment for
example).
Our View holder should register for these view events and call the event
callback function with the relevant info:
In in the Fragment just implement this functions when you create the Adapter
and show a Toast message:
Here we use a different contract then the request permission contract, the
contract will be the OpenDocument() contract in which the launcher receives
an array of strings representing the mime types of the files we want to show to
the user to choose one from. The result is the Uri of the specific file chosen by
the user. Because we launch another component which reads the file storage
and display it to the user for him a choose from, we don’t need to ask for the
reading permission ourself. Instead the activity that actually read the storage
should ask for the permission.
So add the launcher creation to the AddItemFragment and in the callback that
receives the URI of the photo chosen, set this photo in the image view and add
it to the Item instance.
Be aware of the fact that for security reasons this Uri is temporary it is valid
until our activity session will end(until onDestroy()). Because we need to save
it in the file system later on, we need to ask for the OS to make the Uri
persistent. This is done through the Content Resolver component that will be
discussed later on as well as the actual saving of the item in the local DB.
Replace the phot null value with the imageUri in the Item constructor call
In the Pick Photo button click launch you launcher and give the “image/*” mime
type which means images of all types
Now we see the photo in the ImageView but not yet in the recycler. To achieve
this we need to go back to our bind function of the View Holder and use the
external Open Source Glide library to read the image from the Uri stored in The
item class into the image view of the cell. The reason we use Glide is besides of
its incredible images caching and auto resizing that greatly improves
performance it is also doing all of its IO work on a background thread
automatically and update the UI on the main thread - we don’t need to worry
about it - it is also done very efficiently.
So add the Glide dependency to the App grade file and sync your project (you
can find the latest in the Glide GitHub)
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
And in the bind function use it to load the image and make it nice and round
into the image view:
That’s it our project if finished for now. Later on we will move our data to the
View Model, notify about changes in it with the Live Data and make it persistent
with ROOM database.
And that’s it, now we can send it in the Bundle and add it to the list. But the
adapter which also reference this list should know that it’s data source has
changed and it needs to notify the recycler to get updated and read from it the
most relevant data. The are few functions through which the adapter can cause
the recycler to get adapted one is notifyDataSetChanged() which causes the
recycler to read all of the adapter Adam all over again but if have a more
specific change we can use the notifyItemRemoved/Inserted/Updated and
pass them the exact index to refresh the view.
So our code for sending the Item in the Add Item Fragment should look like this: