├── LICENSE ├── LiveData └── LiveData_ObserverPattern.md ├── README.md ├── Repository └── Intro_Repository.md ├── Room └── Creating_Database.md ├── ViewModel └── Introduction_ViewModel.md └── images ├── basic_aac_diagram.png ├── changedFlow.PNG ├── expectedFlow.PNG ├── intro_viewModel.PNG ├── lifecycle_interfaces.png ├── livedata_intro.png ├── observer_pattern.png ├── onFinishAdd.PNG ├── onHomeClick.png ├── room_errordate.png ├── room_features.png ├── runnable_UI.png ├── runnable_thread.png └── screenstays_atupdate.PNG /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Henna Singh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LiveData/LiveData_ObserverPattern.md: -------------------------------------------------------------------------------- 1 | # LiveData and Observer Pattern 2 | 3 | According to the documentation, LiveData is an observable data holder class. LiveData sits between our database and our UI. LiveData is 4 | able to monitor changes in the database and notify the observers when data changes. 5 | 6 | ![liveData intro](/images/livedata_intro.png) 7 | 8 | Similar to the Singleton Pattern, **Observer Pattern** is one of the most common **design patterns** in software development. The classes 9 | called Observers subscribe to what we call, the subject. The subject, which in our case is the LiveData object which will keep a list of 10 | all the Observers that are subsribed to it and notify all of them when there is any relevant change 11 | 12 | ![observer Pattern](/images/observer_pattern.png) 13 | 14 | When our data changes, the set value method on our LiveData object will be called. That will trigger a call to a method in each of the 15 | Observers. Then the Observers will use to do whatever function they need to do, but in this case to use the updated data to update the UI. 16 | 17 | ### Resources 18 | - [Live Data](https://developer.android.com/topic/libraries/architecture/livedata) 19 | - [Observer Pattern](https://en.wikipedia.org/wiki/Observer_pattern) 20 | - [Adding Components to Project](https://developer.android.com/topic/libraries/architecture/adding-components) 21 | 22 | ## Adding LiveData 23 | 24 | To add LiveData and ViewModel, we would need to add relevant dependencies and annotation processors. 25 | ```java 26 | implementation "android.arch.lifecycle:extensions:1.1.0" 27 | annotationProcessor "android.arch.lifecycle:compiler:1.1.0" 28 | ``` 29 | Now lets open our TaskDao interface. The `loadAllTasks()` method returns a list of taskEntry objects. In our current app, this method is called 30 | everytime we need to check if there is any change in the database. It will be more efficient if we get notified when there is a change in 31 | the database which is achieved by returning the object with LiveData 32 | 33 | **Before** 34 | ```java 35 | @Query("SELECT * FROM task ORDER BY priority") 36 | List loadAllTasks(); 37 | ``` 38 | **After** 39 | ```java 40 | @Query("SELECT * FROM task ORDER BY priority") 41 | LiveData> loadAllTasks(); 42 | ``` 43 | We will need to make a similar change in our `retrieveTasks()` method which makes this method call. LiveData simplifies the code as promised. 44 | 45 | The first simplification comes from the fact that LiveData will run by default outside of the main thread. Because of that we can avoid 46 | using the executor. 47 | 48 | **Before** 49 | ```java 50 | private void retrieveTasks() { 51 | AppExecutors.getInstance().diskIO().execute(new Runnable() { 52 | @Override 53 | public void run() { 54 | Log.d(TAG, "Actively retrieving the tasks from the DataBase"); 55 | final List tasks = mDb.taskDao().loadAllTasks(); 56 | runOnUiThread(new Runnable() { 57 | @Override 58 | public void run() { 59 | mAdapter.setTasks(tasks); 60 | } 61 | }); 62 | } 63 | }); 64 | } 65 | ``` 66 | We also will not need `runOnUI()` method for updating the UI. Our task variable is of type LiveData, so we can call its `observe` method. 67 | This method requires 2 parameters, a lifecycle owner(something that has a lifecycle) and an observer. Lifecycle owner here will be our 68 | activity referred as this in code and we will create a new observer. 69 | 70 | This is an interface where we need to implement the onChanged method.The `onChanged()` method will take one parameter which is list 71 | of task entry objects which we are wrapping inside our LiveData object. This `onChanged` method can access the views so we can use it for 72 | the logic that we currently have on `runOnUI` method. 73 | 74 | To sum up, we are calling the LiveData object and call its observe method. This happens out of the main thread by default. We have our logic 75 | to update the UI in onChange method of the observer which runs on the main thread by default. 76 | 77 | **After** 78 | ```java 79 | private void retrieveTasks() { 80 | 81 | Log.d(TAG, "Actively retrieving the tasks from the DataBase"); 82 | 83 | final LiveData> tasks = mDb.taskDao().loadAllTasks(); 84 | 85 | tasks.observe(this, new Observer>() { 86 | @Override 87 | public void onChanged(@Nullable List taskEntries) { 88 | mAdapter.setTasks(taskEntries); 89 | } 90 | }); 91 | } 92 | ``` 93 | We make changes only to Reading Database because we want LiveData to observe changes in the database. For other operations such as INSERT, 94 | UPDATE or DELETE , we do not need to observe changes in the database. For them we will not use LiveData and we will keep executors as is. 95 | 96 | Every change in the database will trigger the onChanged method of the observer so we wont need to call retrieveTask method after 97 | deleting a task. We can also move retrieveTask method call to onCreate from onResume method and delete the onResume() method from activity. 98 | 99 | ### Adding LiveData to AddTaskActivity 100 | 101 | Now we will also make changes in AddTaskActivity and apply LiveData to `loadTaskById()` method when we update a task entry object. Now in TaskDao interface method signature will change like below 102 | 103 | **Before** 104 | ```java 105 | @Query("SELECT * FROM task WHERE id = :id") 106 | TaskEntry loadTaskById(int id); 107 | ``` 108 | **After** 109 | ```java 110 | @Query("SELECT * FROM task WHERE id = :id") 111 | LiveData loadTaskById(int id); 112 | ``` 113 | Now when you come back to AddTaskActivity, there is compiler error and we change the method sginature there as well and remove the exxecutor from place since LiveData run out of the main thread by default. We will update the UI in `onChanged()` method of the task observer. But in this case we do not want to receive updates, so we will remove the observer from our LiveData object. 114 | 115 | **Before** 116 | ```java 117 | Intent intent = getIntent(); 118 | if (intent != null && intent.hasExtra(EXTRA_TASK_ID)) { 119 | mButton.setText(R.string.update_button); 120 | if (mTaskId == DEFAULT_TASK_ID) { 121 | 122 | mTaskId = intent.getIntExtra(EXTRA_TASK_ID, DEFAULT_TASK_ID); 123 | 124 | AppExecutors.getInstance().diskIO().execute(new Runnable() { 125 | @Override 126 | public void run() { 127 | final TaskEntry task = mDb.taskDao().loadTaskById(mTaskId); 128 | runOnUiThread(new Runnable() { 129 | @Override 130 | public void run() { 131 | populateUI(task); 132 | } 133 | }); 134 | } 135 | }); 136 | } 137 | ``` 138 | **After** 139 | ```java 140 | Intent intent = getIntent(); 141 | if (intent != null && intent.hasExtra(EXTRA_TASK_ID)) { 142 | mButton.setText(R.string.update_button); 143 | if (mTaskId == DEFAULT_TASK_ID) { 144 | mTaskId = intent.getIntExtra(EXTRA_TASK_ID, DEFAULT_TASK_ID); 145 | 146 | final LiveData task = mDb.taskDao().loadTaskById(mTaskId); 147 | task.observe(this, new Observer() { 148 | @Override 149 | public void onChanged(@Nullable TaskEntry taskEntry) { 150 | task.removeObserver(this); 151 | populateUI(taskEntry); 152 | } 153 | }); 154 | } 155 | } 156 | ``` 157 | 158 | Now when we run the app and update a task it works as before but when we press the home button and come back to it, we cans see that we are not activity querying the database again, the screen stays. 159 | 160 | ![Screen Stays At Update](/images/screenstays_atupdate.PNG) 161 | 162 | Usually, we do not need to remove the observer as we want LiveData to reflect the state of the underlying data. In our case we are doing a one-time load, and we don’t want to listen to changes in the database. 163 | 164 | Then, why are we using LiveData? Why don’t we keep the executor as it is instead? 165 | 166 | When we progress in the lesson we will move this logic to the ViewModel. There we will benefit from the rest of advantages of LiveData, even if we have used it for a one-time load. 167 | 168 | 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Android-Architecture-Components 3 | To introduce the terminology, here is a short introduction to the Architecture Components and how they work together. Each component is explained more in their respective sections. 4 | 5 | This diagram shows a basic form of this architecture. 6 | ![Basic AAC Diagram](/images/basic_aac_diagram.png) 7 | 8 | *Entity:* When working with Architecture Components, the entity is an annotated class that describes a database table. 9 | 10 | *SQLite database:* On the device, data is stored in a SQLite database. The Room persistence library creates and maintains this database for you. 11 | 12 | *DAO:* Data access object. A mapping of SQL queries to functions. You used to have to define these queries in a helper class, such as `SQLiteOpenHelper`. When you use a DAO, you call the methods, and the components take care of the rest. 13 | 14 | *Room database:* Database layer on top of SQLite database that takes care of mundane tasks that you used to handle with a helper class, such as `SQLiteOpenHelper`. Provides easier local data storage. The Room database uses the DAO to issue queries to the SQLite database. 15 | 16 | *Repository:* A class that you create for managing multiple data sources, for example using the WordRepository class. 17 | 18 | *ViewModel:* Provides data to the UI and acts as a communication center between the Repository and the UI. Hides the backend from the UI. `ViewModel` instances survive device configuration changes. 19 | 20 | *LiveData:* A data holder class that follows the observer pattern, which means that it can be observed. Always holds/caches latest version of data. Notifies its observers when the data has changed. `LiveData` is lifecycle aware. UI components observe relevant data. `LiveData` 21 | automatically manages stopping and resuming observation, because it's aware of the relevant lifecycle status changes. 22 | -------------------------------------------------------------------------------- /Repository/Intro_Repository.md: -------------------------------------------------------------------------------- 1 | # The Repository 2 | At the moment, we have our database logic in our Activities and our ViewModels. We could improve our architecture by extracting all that logic to a common place, or Repository. This way, all database-related operations can be handled from a single location, while being accessible from anywhere in our app. This is an example of using the [Single Responsibility Principle](https://en.wikipedia.org/wiki/Single_responsibility_principle). 3 | 4 | Using a Repository also adds more flexibility to our architecture. Instead of touching code in multiple places whenever we want to change the database, we can instead isolate the code change to one place using a Repository. 5 | -------------------------------------------------------------------------------- /Room/Creating_Database.md: -------------------------------------------------------------------------------- 1 | # Introduction to Room 2 | Room is an ORM - object relational mapping library. Room will map our database objects to Java objects. Using Room we will wrote less boilerplate and benefit from SQL validation at compile time. 3 | 4 | ![room features](/images/room_features.png) 5 | 6 | Room uses annotations and has 3 main components 7 | 1. @Entity - to define our database tables 8 | 2. @DAO(data access object) - to provide an API for reading and writing data 9 | 3. @Database - which represents a database holder 10 | 11 | This will include a list of entities and DAO and will allow us to create a new database or to acquire a connection to our db at runtime 12 | 13 | ### Benefits of Using Room 14 | If your app handles non-trivial amounts of data, you can gain a number of benefits by storing at least some of that data locally using Room. The most common benefit is to optimize network connectivity, by caching relevant data to ensure users can still browse that content while they are offline. Any changes the user makes to content can later be synced to the server, once the device is back online. 15 | 16 | The core Android framework provides built-in support for working with raw SQL data. While the built-in APIs are very powerful, they also present a number of development challenges: 17 | - They are relatively low-level, and require a large amount of development time and effort. 18 | - Raw SQL queries are not verified at compile-time. 19 | - You must manually update SQL queries to reflect changes in your data graph. This process is unnecessarily time consuming, and error-prone. 20 | - You write and maintain a lot of boilerplate code to convert between SQL queries and data objects. 21 | 22 | Room is designed to abstract away the underlying database tables and queries, and encourage best-practice development patterns on Android. 23 | 24 | By default, Room doesn't allow you to issue database queries on the main thread to avoid poor UI performance. However querying on the main thread is enabled in sometimes for simplicity. 25 | 26 | ## Creating an Entity 27 | 28 | Adding Room dependencies to gradle file 29 | ```java 30 | implementation 'android.arch.persistence.room:runtime:1.0.0' 31 | annotationProcessor 'android.arch.persistence.room:compiler:1.0.0' 32 | ``` 33 | 34 | ### Plain Old Java Object 35 | 36 | We will create a POJO class, here its called `TaskEntry.java`. The class has member variables, getters and setters, and constructors. The task here is to turn this class into an entity. This is done by adding @Entity annotation to our class, this way room will generate a table called task entry but we do not need to have java classes and database tables with the same name. 37 | 38 | The way this is handled in Room is to optionally assign a specific table name to our entity. Having a look at the member variables- Id, description, priority, updatedAt; each of them will match a column in the associated table for this entity. 39 | Sometimes we may also like to add extra fields to our class that are not part of the table. When that happens we need to let Room know by using the `ignore` annotation. One more requirement for Room is to define a primary key which will be id of task. 40 | With this Id field will be unique, its preferrable to get database to do it for us, so we set autoGenrate value to true. 41 | 42 | ```java 43 | @Entity (tableName = "task") 44 | public class TaskEntry { 45 | 46 | @PrimaryKey (autoGenerate = true) 47 | private int id; 48 | private String description; 49 | private int priority; 50 | private Date updatedAt; 51 | 52 | @Ignore 53 | public TaskEntry(String description, int priority, Date updatedAt) { 54 | this.description = description; 55 | this.priority = priority; 56 | this.updatedAt = updatedAt; 57 | } 58 | 59 | public TaskEntry(int id, String description, int priority, Date updatedAt) { 60 | this.id = id; 61 | this.description = description; 62 | this.priority = priority; 63 | this.updatedAt = updatedAt; 64 | } 65 | 66 | public int getId() { 67 | return id; 68 | } 69 | 70 | public void setId(int id) { 71 | this.id = id; 72 | } 73 | 74 | public String getDescription() { 75 | return description; 76 | } 77 | 78 | public void setDescription(String description) { 79 | this.description = description; 80 | } 81 | 82 | public int getPriority() { 83 | return priority; 84 | } 85 | 86 | public void setPriority(int priority) { 87 | this.priority = priority; 88 | } 89 | 90 | public Date getUpdatedAt() { 91 | return updatedAt; 92 | } 93 | 94 | public void setUpdatedAt(Date updatedAt) { 95 | this.updatedAt = updatedAt; 96 | } 97 | } 98 | ``` 99 | One of the limitations of room is that it can only use one constructor but we have 2, one with ID and one without it. The first constructor is used when we write to the database because the ID is auto-generated and is not known to us while second is needed when we read from the database, each entry will already have an ID. To let room know it has to use second constructor, we add ignore annotation to the first one. 100 | 101 | ### Additinal Resources 102 | 103 | - [SQL Cheatsheet](https://d17h27t6h515a5.cloudfront.net/topher/2016/September/57ed880e_sql-sqlite-commands-cheat-sheet/sql-sqlite-commands-cheat-sheet.pdf) 104 | - [W3School SQL Tutorial](https://www.w3schools.com/sql/) 105 | 106 | ## Creating a DAO 107 | 108 | We will now create a Dao or data access object for each of our entities. This is our Task DAO 109 | 110 | ```java 111 | @Dao 112 | public interface TaskDao { 113 | 114 | @Query("SELECT * FROM task ORDER BY priority") 115 | List loadAllTasks(); 116 | 117 | @Insert 118 | void insertTask(TaskEntry taskEntry); 119 | 120 | @Update(onConflict = OnConflictStrategy.REPLACE) 121 | void updateTask(TaskEntry taskEntry); 122 | 123 | @Delete 124 | void deleteTask(TaskEntry taskEntry); 125 | } 126 | ``` 127 | Observations 128 | 1. It is an interface that has Dao annotation. 129 | 2. We have methods for each of CRUD operations and relevant annotations are added to each of these methods. 130 | 3. The first method includes the query annotation to run SQL command that will return a list of task entry objects. 131 | 132 | The fact that we can request objects back, is what makes room an object relational mapping library. The rest of the methods take a TaskEntry object as a parameter. The update task method annotated with update is set to replace in case of conflict 133 | 134 | ## Creating a Database 135 | 136 | We learnt to create database table (entity) and DAO (queries to access the table ) and now we will create a database that used both of them. The standard code to create a database is as follows 137 | 138 | ```java 139 | public abstract class AppDatabase extends RoomDatabase { 140 | 141 | private static final String LOG_TAG = AppDatabase.class.getSimpleName(); 142 | private static final Object LOCK = new Object(); 143 | private static final String DATABASE_NAME = "todolist"; 144 | private static AppDatabase sInstance; 145 | 146 | public static AppDatabase getInstance(Context context) { 147 | if (sInstance == null) { 148 | synchronized (LOCK) { 149 | Log.d(LOG_TAG, "Creating new database instance"); 150 | sInstance = Room.databaseBuilder(context.getApplicationContext(), 151 | AppDatabase.class, AppDatabase.DATABASE_NAME) 152 | .build(); 153 | } 154 | } 155 | Log.d(LOG_TAG, "Getting the database instance"); 156 | return sInstance; 157 | } 158 | 159 | ``` 160 | 161 | The getInstance method will return an AppDatabase using the Singleton pattern. The Singleton pattern is a software design pattern that restricts the instantiation of a class to one object. This is useful when we want to ensure that only one instance of a given class is created. 162 | 163 | The first call when sInstance is not null will instantiate a new value and being a static variable the value will be retained throughout app lifecycle and subsequent calls will only return sInstance object without creating a new instance. 164 | The database is created if it does not already exists, if it exist then a connection to the database is established. 165 | 166 | We annotate the class with database notation. In it we will provide the list of classes that we have annotated as entities for our database, in this case there is only 1 class. 167 | 168 | ```java 169 | @Database(entities = {TaskEntry.class}, version = 1, exportSchema = false) 170 | public abstract class AppDatabase extends RoomDatabase { 171 | ``` 172 | The version will change if we update the db, export is not necessary in this case but is mentioned to avoid warnings. Since we had only one entity, we mentioned that. 173 | 174 | To add Task DAO we will define an abstract method that will returns it `public abstract TaskDao taskDao();` 175 | 176 | But if you try to build your project at this stage, you will find that it does not compile, it seems like Room cannot figure out how to save one of the fields in the database 177 | 178 | ![room date error](/images/room_errordate.png) 179 | 180 | On double clicking the error, we see the field that is causing the problem is updatedAt. Looking at documentation of different data types in SQL helps finding the exact prob and its solution . 181 | 182 | - [Data Types](https://www.sqlite.org/datatype3.html) 183 | 184 | From the documentation, we know date can be of 3 types 185 | - Real, 186 | - Text or 187 | - Integer 188 | Room needs to match each of our fields to one of these datatypes, Strings and numbers are OK, but Room cannot automatically map more complex extractors like Dates. It is in cases like these when type converters come to play. The error msg also suggests to use typeconverters. 189 | Below is DateConverter class 190 | 191 | ```java 192 | public class DateConverter { 193 | @TypeConverter 194 | public static Date toDate(Long timestamp) { 195 | return timestamp == null ? null : new Date(timestamp); 196 | } 197 | 198 | @TypeConverter 199 | public static Long toTimestamp(Date date) { 200 | return date == null ? null : date.getTime(); 201 | } 202 | } 203 | ``` 204 | Both methods in the class are annotated with TypeConverter 205 | 1. The first one receives a timestamp and converts it to a date object. Room will use this method when reading from the database 206 | 2. The second one receives a date object and converts it into a timestamp long. Room will use this method when writing into the database 207 | 208 | For Room to use this TypeConverter , we will add TypeConverter annotation to our AppDatabase class created above 209 | 210 | ```java 211 | @Database(entities = {TaskEntry.class}, version = 1, exportSchema = false) 212 | @TypeConverters(DateConverter.class) 213 | public abstract class AppDatabase extends RoomDatabase { 214 | ``` 215 | Now Room knows how to deal with the database. Now the code compiles without any problems. 216 | 217 | ### Saving a Task 218 | 219 | Accessing the database in the main thread can be time consuming, and could lock the UI and throw an ANR (Application Not Responding) error. To avoid that, Room will, by default, throw an error if you attempt to access the database in the main thread. 220 | 221 | In the finished app, we need to implement the database operations to run asynchronously, but we also need to validate that what we have done so far is working, so let’s temporarily enable the option to allow queries in the main thread. 222 | 223 | For doing that we will call a method in our AppDatabase class when we create database instance. 224 | 225 | ```java 226 | sInstance = Room.databaseBuilder(context.getApplicationContext(), 227 | AppDatabase.class, AppDatabase.DATABASE_NAME) 228 | .allowMainThreadQueries() 229 | .build(); 230 | ``` 231 | #### Steps to save data in the database 232 | 1. Open AddTaskActivity and create an instance of AppDatabase class 233 | 2. Initialize the variable in Activity onCreate method ` mAppDatabase = AppDatabase.getInstance(this);` 234 | 3. The `onSaveButtonClicked()` method will be called add Task is clicked from the UI 235 | 4. To insert the data in the database, we would need to get details what user input in the UI. 236 | 5. With these details we can call the constructor of TaskEntry class. 237 | 6. We will retrieve taskDao from our database instance and call the insertTask method using the task entry object that we created. 238 | 239 | ```java 240 | public void onSaveButtonClicked() { 241 | 242 | String description = mEditText.getText().toString(); 243 | int priority = getPriorityFromViews(); 244 | Date date = new Date(); 245 | 246 | TaskEntry taskEntry = new TaskEntry(description,priority,date); 247 | mAppDatabase.taskDao().insertTask(taskEntry); 248 | finish(); 249 | } 250 | ``` 251 | 252 | ### Reading the List of Tasks 253 | Now that we have saved tasks in our database we would query the database to display all data in the UI 254 | 255 | 1. Add a AppDatabase variable and initialize it in the `onCreate()` method of the Activity 256 | 2. We can query the data in `onCreate()` but it will never be refreshed unless our activity is recreated. The alternate is to query the database in `onResume()` method 257 | 3. We call loadAllTasks method from our taskDao and resulting list is passed to the adapter using setTasks method 258 | 259 | ```java 260 | @Override 261 | protected void onResume() { 262 | super.onResume(); 263 | mAdapter.setTasks(mAppDatabase.taskDao().loadAllTasks()); 264 | } 265 | ``` 266 | 267 | ### Threads and Runnables 268 | 269 | Now that we know that our implementation works, we will disable queries in the main UI Thread. As we know certain operations can block the UI, we will run these operations asynchronously and in a separate thread. 270 | 271 | One way to get off the main thread is to make a runnable in a separate thread, which will look like this 272 | 273 | ![runnable thread](/images/runnable_thread.png) 274 | 275 | We create a new thread that uses a runnable. The runnable will perform our database logic, if its needed it will update the UI and finally we start the thread to execute the logic. 276 | 277 | The issue with this approach is that UI cannot be modified from the newly created thread. But thanks to Architecture Components, we will handle UI in a completely different way. The temporary solution now would be to run on UI thread with a new runnable to update our UI which will look like below 278 | 279 | ![UI on runnable](/images/runnable_UI.png) 280 | 281 | ### Executors 282 | 283 | We discussed implementing database logic as below, this means we create a new thread every time that we need to use the database. Once the thread is done, it will be garbage collected. 284 | 285 | ```java 286 | Thread thread = new Thread(new Runnable(){ 287 | @Override 288 | public void run() { 289 | //Database Logic 290 | } 291 | }); 292 | thread.start(); 293 | ``` 294 | This process will be repeated many times. We may create [race conditions](https://en.wikipedia.org/wiki/Race_condition) if various threads run on the same time. To avoid all these problems it would be great to have all our database calls in the same thread. That will ensure our calls are done sequentially and will avoid creating thread every time. Here is where [**Executors**](https://developer.android.com/reference/java/util/concurrent/Executor) come into play. 295 | 296 | An Executor is an object that executes as submitted runnable tasks. It is normally used instead of explicitly creating threads for each of a set of tasks. 297 | 298 | AppExecutors class look like below 299 | 300 | ```java 301 | public class AppExecutors { 302 | 303 | // For Singleton instantiation 304 | private static final Object LOCK = new Object(); 305 | private static AppExecutors sInstance; 306 | private final Executor diskIO; 307 | private final Executor mainThread; 308 | private final Executor networkIO; 309 | 310 | private AppExecutors(Executor diskIO, Executor networkIO, Executor mainThread) { 311 | this.diskIO = diskIO; 312 | this.networkIO = networkIO; 313 | this.mainThread = mainThread; 314 | } 315 | 316 | public static AppExecutors getInstance() { 317 | if (sInstance == null) { 318 | synchronized (LOCK) { 319 | sInstance = new AppExecutors(Executors.newSingleThreadExecutor(), 320 | Executors.newFixedThreadPool(3), 321 | new MainThreadExecutor()); 322 | } 323 | } 324 | return sInstance; 325 | } 326 | 327 | public Executor diskIO() { 328 | return diskIO; 329 | } 330 | 331 | public Executor mainThread() { 332 | return mainThread; 333 | } 334 | 335 | public Executor networkIO() { 336 | return networkIO; 337 | } 338 | 339 | private static class MainThreadExecutor implements Executor { 340 | private Handler mainThreadHandler = new Handler(Looper.getMainLooper()); 341 | 342 | @Override 343 | public void execute(@NonNull Runnable command) { 344 | mainThreadHandler.post(command); 345 | } 346 | } 347 | 348 | } 349 | ``` 350 | This class contains 3 executors, but in our ToDO list app we will use only disk IO executor. The class constructor receives the three executors as parameters in this order - diskIO, networkIO, and mainThread. 351 | 352 | The **disk IO** is a single thread executor. This ensures that our database transactions are done in order, so we do not have race conditions. 353 | 354 | The **network IO** is a pool of three threads. This allows us to run different network calls simultaneously while controlling the number of threads that we have. 355 | 356 | Finally the **main thread** executor uses the main thread executor class which essentially will post runnables using a handle associated with the main looper. When we are in an activity we do not need this main thread executor because we can use the run on UI thread method. When we are in a different class and we do not have the run on UI thread method, we can access the main thread using this last executor. As its not possible to imagine a situation where this can be used, here is an [example](https://github.com/googlesamples/android-architecture-components/blob/b1a194c1ae267258cd002e2e1c102df7180be473/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/NetworkBoundResource.java) 357 | 358 | Now we will use diskIO executor to perform our database operations. We used `onSaveButtonClicked()` method to add tasks to our database, we can use the same method to get an instance of our app executors and access our diskIO executor to execute a new runnable and that will contain our database logic. 359 | 360 | The newly created will look like this 361 | 362 | ```java 363 | public void onSaveButtonClicked() { 364 | String description = mEditText.getText().toString(); 365 | int priority = getPriorityFromViews(); 366 | Date date = new Date(); 367 | 368 | //final so that its accessible from inside run method 369 | final TaskEntry taskEntry = new TaskEntry(description, priority, date); 370 | AppExecutors.getInstance().diskIO().execute(new Runnable() { 371 | @Override 372 | public void run() { 373 | mDb.taskDao().insertTask(taskEntry); 374 | finish(); 375 | } 376 | }); 377 | } 378 | ``` 379 | Similarly in our mainActivity we will call the executor to load the tasks in our `onResume()` method. 380 | 1. We will get App executor instance and then execute a new runnable that will query the database to retrieve a list of task entry objects. 381 | 2. We wont be able to pass the list to the adapter from the thread in diskIO executor. We will need to wrap it inside a runOnUI() thread method call 382 | 383 | ```java 384 | @Override 385 | protected void onResume() { 386 | super.onResume(); 387 | 388 | AppExecutors.getInstance().diskIO().execute(new Runnable() { 389 | @Override 390 | public void run() { 391 | final List tasks = mDb.taskDao().loadAllTasks(); 392 | //we will be able to simplify this when we learn about AAC 393 | runOnUiThread(new Runnable() { 394 | @Override 395 | public void run() { 396 | mAdapter.setTasks(mDb.taskDao().loadAllTasks()); 397 | } 398 | }); 399 | } 400 | }); 401 | } 402 | ``` 403 | ### Deleting from Database 404 | 405 | For now in the app we have an ItemTouchHelper() attached to our RecyclerView for deleting the task. But on swiping, it deletes the task but row still remains there and we relaunch the app, the data reappears. 406 | 407 | ```java 408 | new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0, 409 | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { 410 | @Override 411 | public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, 412 | RecyclerView.ViewHolder target) { 413 | return false; 414 | } 415 | 416 | // Called when a user swipes left or right on a ViewHolder 417 | @Override 418 | public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) { 419 | // Here is where you'll implement swipe to delete 420 | } 421 | }).attachToRecyclerView(mRecyclerView); 422 | ``` 423 | The OnSwipe() method of the call back will be called when the user swipes. This method receives 2 parameters 424 | - viewHolder for that row 425 | - integer that describes the swipe direction. We may want to use swipe direction to implement different actions. But in our case we will delete the task from our database in any case. 426 | 427 | #### Steps to delete 428 | 1. Get an instance of diskIO executor from the AppExecutors and use it to execute a new runnable 429 | 2. We need to find out which task to delete, activity does not hold any references to the list of tasks. The object that holds that know how is our adapter. To access the lists of tasks we will need to add a public getTasks method to our adapter 430 | 431 | ```java 432 | public List getTasks(){ 433 | return mTaskEntries; 434 | } 435 | ``` 436 | using the method above we can retrieve list of task entry objects from our adapter. To get the exact task we need to know which position was swiped. Back in main activity we will get this result by getting position from viewHolder. 437 | 3. We can then call the delete method of task DAO and pass in the task entry object with that position. While we deleted the task from the database , we will need to update the UI 438 | 4. So instead of duplicating the code in onResume() method we will separate the code in a separate method and call that. 439 | 440 | ```java 441 | @Override 442 | public void onSwiped(final RecyclerView.ViewHolder viewHolder, int swipeDir) { 443 | 444 | AppExecutors.getInstance().diskIO().execute(new Runnable() { 445 | @Override 446 | public void run() { 447 | int position = viewHolder.getAdapterPosition(); 448 | List tasks = mAdapter.getTasks(); 449 | mDb.taskDao().deleteTask(tasks.get(position)); 450 | retrieveTasks(); 451 | } 452 | }); 453 | ``` 454 | ### Updating a Task 455 | 456 | This is similarly implementented like all other tasks but when an item is clicked for update. In our app we have `onItemClickListener()` method which receives item ID as a parameter. We will put the item ID in the Intent and we will query the database to get that task. 457 | 458 | But if we observe our TaskDao interface above we do not have a method that could retrieve a task given its task ID so we will create one. Our new method will take ID as parameter and return taskEntry object. As this method is meant to retrieve our task from the database we will add ROOM query annotation with the apt SQL statement 459 | 460 | ```java 461 | @Query("SELECT * FROM task where id= :id") 462 | TaskEntry loadTaskById(int id); 463 | ``` 464 | Important feature of ROOM here is that ROOM includes SQL validation at compile time, so if you accidently write a wrong table/column name , the app build will fail. 465 | 466 | #### Steps to update 467 | 1. Use `onItemClickListener()` method to retrieve the task ID and pass it as an extra to AddTask Activity with key EXTRA_TASK_ID 468 | ```java 469 | @Override 470 | public void onItemClickListener(int itemId) { 471 | // Launch AddTaskActivity with itemId as extra for the key AddTaskActivity.EXTRA_TASK_ID 472 | Intent addTask = new Intent(this, AddTaskActivity.class); 473 | addTask.putExtra(AddTaskActivity.EXTRA_TASK_ID,itemId); 474 | startActivity(addTask); 475 | } 476 | ``` 477 | 2. In Add Task Activity, retrieve the extra task id and assign it to id variable which will be used to retrieve the task from database 478 | 3. Similar to previous steps we will use disk IO executor to get the appropriate task from database and then we will pass the task object received to `populateUI` method but we cannot update the UI from inside run method , so we will call `runOnUI` method 479 | 480 | ```java 481 | Intent intent = getIntent(); 482 | if (intent != null && intent.hasExtra(EXTRA_TASK_ID)) { 483 | mButton.setText(R.string.update_button); 484 | if (mTaskId == DEFAULT_TASK_ID) { 485 | mTaskId = intent.getExtras().getInt(EXTRA_TASK_ID,DEFAULT_TASK_ID); 486 | 487 | AppExecutors.getInstance().diskIO().execute(new Runnable() { 488 | @Override 489 | public void run() { 490 | final TaskEntry taskEntry = mDb.taskDao().loadTaskById(mTaskId); 491 | runOnUiThread(new Runnable() { 492 | @Override 493 | public void run() { 494 | populateUI(taskEntry); 495 | } 496 | }); 497 | } 498 | }); 499 | } 500 | } 501 | ``` 502 | 4. Now for `updateUI` method, return if the task is null else set appropriate values to UI components (priority and edit text in this case) 503 | 504 | ```java 505 | private void populateUI(TaskEntry task) { 506 | 507 | if(task ==null) return; 508 | 509 | mEditText.setText(task.getDescription()); 510 | setPriorityInViews(task.getPriority()); 511 | } 512 | ``` 513 | 5. Now we will also add update logic to `onSaveButtonClicked` method, we will write an if else statement in the run method so that task is only inserted if it matched the default ID else its updated else set the task ID and update the task 514 | 515 | ```java 516 | public void run() { 517 | if (mTaskId == DEFAULT_TASK_ID) { 518 | mDb.taskDao().insertTask(taskEntry); 519 | } else { 520 | taskEntry.setId(mTaskId); 521 | mDb.taskDao().updateTask(taskEntry); 522 | } 523 | finish(); 524 | } 525 | ``` 526 | 527 | ## Observations on the current state of the App 528 | The app works fine with all the CRUD operations, but it is not as good as it seems. If we look at our `onRetrieveTasks()` method in main activity and add some logging to it, we will see how the msg appears on the logcat everytime onResume is called. We would need to call this method only if there has been change in the database but for now its called everytime. 529 | 530 | Most of the time we will be Re-quering the database to find out that there has not been any changes which is quite inefficient and generates an unnecessary waste of battery. It is here where Live Data comes to the rescue 531 | 532 | --------------------------------------------------------------------------------------------------------------------------------------- 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | -------------------------------------------------------------------------------- /ViewModel/Introduction_ViewModel.md: -------------------------------------------------------------------------------- 1 | # Introduction to ViewModel 2 | 3 | Thanks to LiveData we are notified of changes in our database without needing to be continuously re-querying. Unfortunately we 4 | are still re-querying the database everytime that we rotate our device. 5 | 6 | We know activities are destroyed and re-created on rotation, this means that `onCreate()` will be called again, a new livedata object 7 | will be created and for that there will be new call to the database. 8 | 9 | The usual approach of using `onSaveInstanceState()` is meant for a small amounts of data that can be easily serialized or deserialized 10 | but that does not happen in this case. The alternative is querying the database, but doing it everytime on configuration change is not 11 | efficient. 12 | 13 | It is where **ViewModel** comes in. ViewModel allows data to survive configuration changes such as rotation. As depicted in the diagram 14 | below the lifecycle of ViewModel starts when the activity is created and lasts until it is finished. 15 | 16 | ![ViewModel lifecycle](/images/intro_viewModel.PNG) 17 | 18 | Because of that we can cache complex data in our ViewModel. When the activity is recreated after rotation, it will use exact same 19 | ViewModel, object where we already have our cache data. But it is common that we make asynchronous calls to retrieve our data. 20 | Without the ViewModel if the activity is destroyed if the call finishes, we could have memory leaks. Making sure we do not have memory 21 | leaks is extra overhead. 22 | 23 | Instead we make this asynchronous call from the ViewModel, the result will be delivered back to ViewModel, and as it does not matter 24 | if the activity has been destroyed or not, there will be no memory leaks we need to worry about. 25 | 26 | #### Resources 27 | - [ViewModel Overview](https://developer.android.com/topic/libraries/architecture/viewmodel) 28 | - [Serialization](https://en.wikipedia.org/wiki/Serialization) 29 | - [Parcelable vs Serializable](https://android.jlelse.eu/parcelable-vs-serializable-6a2556d51538) 30 | 31 | ### Steps to add a ViewModel to MainActivity 32 | 33 | 1. Create a new class called MainViewModel or any name and extend from AndroidViewModel 34 | 2. You are required to implement a constructor which takes one parameter of type Application. Since this ViewModel is used to cache our 35 | list of task entry objects wrap in a liveData object, we will create it as private variable `private LiveData> tasks` 36 | which will have a public getter and initialize this variable in our constructor getting an instance of AppDatabase. 37 | 38 | ```java 39 | public class MainViewModel extends AndroidViewModel { 40 | 41 | private LiveData> tasks; 42 | 43 | public MainViewModel(@NonNull Application application) { 44 | super(application); 45 | tasks = AppDatabase.getInstance(this.getApplication()).taskDao().loadAllTasks(); 46 | } 47 | 48 | public LiveData> getTasks() { 49 | return tasks; 50 | } 51 | } 52 | } 53 | ``` 54 | Now we will use this ViewModel in our MainActivity in `retriveTasks` method. Since we are querying the database in our ViewModel, we do 55 | not need to call the `loadAlltask` method from here. We will remove this and get the ViewModel by calling ViewModels's providers of 56 | this activity and pass the ViewModel class as a parameter. 57 | Now we can retrieve our livedata object using the getTasks method from the ViewModel. 58 | 59 | **Before** 60 | ```java 61 | private void retrieveTasks() { 62 | LiveData> tasks = mDb.taskDao().loadAllTasks(); 63 | tasks.observe(this, new Observer>() { 64 | @Override 65 | public void onChanged(@Nullable List taskEntries) { 66 | Log.d(TAG, "Receiving database update from LiveData"); 67 | mAdapter.setTasks(taskEntries); 68 | } 69 | }); 70 | } 71 | ``` 72 | **After** 73 | ```java 74 | private void setUpViewModel() { 75 | MainViewModel viewModel = ViewModelProviders.of(this).get(MainViewModel.class); 76 | viewModel.getTasks().observe(this, new Observer>() { 77 | @Override 78 | public void onChanged(@Nullable List taskEntries) { 79 | Log.d(TAG, "Updating lists of tasks from LiveData in ViewModel"); 80 | mAdapter.setTasks(taskEntries); 81 | } 82 | }); 83 | } 84 | ``` 85 | Similary we will add ViewModel to our update method in **AddTask Activity**. But for updating we need to send task ID parameter to ViewModel, for that we need to create a ViewModel factory. 86 | 87 | So we need to create a new class called AddTaskViewModelFactory which will extend from ViewModel provider NewInstanceFactory. This class will have 2 member variables, instance of AppDatabase and Id of the task we want to update and initialize them in class constructor. 88 | 89 | We will override the create method to return a new AddTaskViewModel(a new class which we will create similar to MainViewModel class) that uses parameters in its constructor. This is a standard code that you can re-use with minor modifications. 90 | 91 | ```java 92 | public class AddTaskViewModelFactory extends ViewModelProvider.NewInstanceFactory { 93 | 94 | private final AppDatabase mAppDatabase; 95 | private final int mTaskId; 96 | 97 | public AddTaskViewModelFactory(AppDatabase appDatabase, int taskId) { 98 | mAppDatabase = appDatabase; 99 | mTaskId = taskId; 100 | } 101 | // Note: This can be reused with minor modifications 102 | @Override 103 | public T create(Class modelClass) { 104 | //noinspection unchecked 105 | return (T) new AddTaskViewModel(mAppDatabase, mTaskId); 106 | } 107 | } 108 | ``` 109 | Now we will create AddTaskViewModel class but since we are using ViewModelFactory we will extend this class from ViewModel instead. Similary as in MainViewModel class , this class will have a private member variable for live data and a public getter. We will initialize our task variable in the constructor with a relevant call to our database. 110 | ```java 111 | //since we using a factory, we will extend from ViewModel instead 112 | public class AddTaskViewModel extends ViewModel{ 113 | 114 | private LiveData task; 115 | 116 | // Note: The constructor should receive the database and the taskId 117 | public AddTaskViewModel(AppDatabase mDb, int taskId) { 118 | task = mDb.taskDao().loadTaskById(taskId); 119 | } 120 | 121 | public LiveData getTask() { 122 | return task; 123 | } 124 | } 125 | ``` 126 | Now coming back to AddTaskActivity, we do not need live data object in the onCreate method now since the call to load task Id is now done on the ViewModel, so we can remove from the code. 127 | **Before** 128 | ```java 129 | Intent intent = getIntent(); 130 | if (intent != null && intent.hasExtra(EXTRA_TASK_ID)) { 131 | mButton.setText(R.string.update_button); 132 | if (mTaskId == DEFAULT_TASK_ID) { 133 | // populate the UI 134 | mTaskId = intent.getIntExtra(EXTRA_TASK_ID, DEFAULT_TASK_ID); 135 | 136 | final LiveData task = mDb.taskDao().loadTaskById(mTaskId); 137 | task.observe(this, new Observer() { 138 | @Override 139 | public void onChanged(@Nullable TaskEntry taskEntry) { 140 | task.removeObserver(this); 141 | Log.d(TAG, "Receiving database update from LiveData"); 142 | populateUI(taskEntry); 143 | } 144 | }); 145 | } 146 | } 147 | ``` 148 | We will create an instance of our factory by passing database and task id to its constructor. ViewModel will be created similar t what we created in MainViewModel with the difference that factory will be added as provider to it. We will use model `getTask()` method to retrieve the live data that we want to observe 149 | **After** 150 | ```java 151 | Intent intent = getIntent(); 152 | if (intent != null && intent.hasExtra(EXTRA_TASK_ID)) { 153 | mButton.setText(R.string.update_button); 154 | if (mTaskId == DEFAULT_TASK_ID) { 155 | // populate the UI 156 | mTaskId = intent.getIntExtra(EXTRA_TASK_ID, DEFAULT_TASK_ID); 157 | 158 | AddTaskViewModelFactory factory = new AddTaskViewModelFactory(mDb, mTaskId); 159 | 160 | final AddTaskViewModel viewModel = ViewModelProviders.of(this, factory).get(AddTaskViewModel.class); 161 | viewModel.getTask().observe(this, new Observer() { 162 | @Override 163 | public void onChanged(@Nullable TaskEntry taskEntry) { 164 | viewModel.getTask().removeObserver(this); 165 | Log.d(TAG, "Receiving database update from LiveData"); 166 | populateUI(taskEntry); 167 | } 168 | }); 169 | } 170 | } 171 | ``` 172 | Now , live data object is cast in the ViewModel. So, we can retrieve it again after rotation without needing to requery our database. 173 | 174 | ## Lifecycle Surprise 175 | Lets discuss the lifecycle of our activities and see how ViewModel takes care of everything 176 | 177 | We are in our AddTaskActivity and we click the add button to create a new task, the flow of data is like below 178 | 1. Task is saved in our db 179 | 2. Our LiveData in our main view model will be updated 180 | 3. That will notify the observer in the main activity via the onChange method 181 | 4. Finally our UI will be updated 182 | ![Expected Flow](/images/expectedFlow.PNG) 183 | 184 | But what happens if we are not in the activity that receives the update ? This is not uncommon and can happen because of various reasons 185 | but we will force it to happen in this case, in our `onSaveButtonClicked()` of the activity we will comment the code `finish()` at the end and run the app. We will see the app stays on the AddTask page and activity is not closed. What will be the flow in this case? 186 | 187 | ![on deleting Finish code](/images/onFinishAdd.PNG) 188 | 189 | As per the expected flow 190 | 1. The task should have been saved in the db 191 | 2. The LiveData in our main view model should have been updated 192 | 3. A call to the onChange method of the observer, that we have in our main activity **but** code in our activity cannot be executed if we are not in the activity *Activities cannot have their code run in the background* 193 | ##### If we are still in the AddTask Activity, how does this effect us? 194 | 195 | ![changed flow](/images/changedFlow.PNG) 196 | 197 | If LiveData has tried to call the onChange method of the observer in main activity, that code has not been executed and so the UI in main activity has also not been updated. But if we press the back button, surprisingly *we see the task is added* (this also happens if phone is shut down accidently, because my emulator did) **How is this possible ?** 198 | 199 | ![On navigating back](/images/onHomeClick.PNG) 200 | 201 | This is possible because of another element of Android Architecture components called lifecycle. **How you may ask?** 202 | We are using LiveData and it already supports lifecycle out of the box. LiveData is a lifecycle aware component, live data is able to know the state of its associated component which in our case is the activity. 203 | 204 | If the activity is started or resumed, then its considered active, and only in that case its observers will be notified. When we our in Add Task Activity when the database is updated, then main activity is not active, that means then live data will not notify the observers in main activity. Once we press the back button, main activity becomes active again and live data notifies its observers so the UI is updated. 205 | 206 | Another benefit of using live data is it also know when the state of the activity is destroyed, and when that happens it will automatically unsubscribe the observers for us to avoid memory leaks. 207 | 208 | So *How did LiveData manage to get these amazing super powers?* 209 | 210 | As we mentioned lifecycle is one of the architecture components and lifecycle will allow non-lifecycle objects to be lifecycle aware and that the different Android architecture components will decide to fit together as building blocks. 211 | 212 | Lifecycle has two interfaces; Lifecycle Owner and Lifecycle Observers 213 | 214 | ![Lifecycle interfaces](/images/lifecycle_interfaces.png) 215 | 216 | **Lifecycle Owners** 217 | They are objects that have a lifecycle like activities and fragments 218 | 219 | **Lifecycle Observers** 220 | They can observe Lifecycle Owners, and get notified on lifecycle changes. LiveData is lifecycle aware because it is LifecycleObserver. If you remember when we added live data in the class we called its observe method which took two parameters, we let it know which LifecycleOwner it should observe by passing it as a parameter. 221 | 222 | You can also implement your own LifecycleObservers to match your needs 223 | 224 | #### Additional Resources 225 | - [Handling Lifecycles with Lifecycle-Aware Components](https://developer.android.com/topic/libraries/architecture/lifecycle) 226 | - [Use Cases for lifecycle-aware components](https://developer.android.com/topic/libraries/architecture/lifecycle#use-cases) 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | -------------------------------------------------------------------------------- /images/basic_aac_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hennasingh/Android-Architecture-Components/9a9e29946752e5feeee9b8f11d123e15cf287ab3/images/basic_aac_diagram.png -------------------------------------------------------------------------------- /images/changedFlow.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hennasingh/Android-Architecture-Components/9a9e29946752e5feeee9b8f11d123e15cf287ab3/images/changedFlow.PNG -------------------------------------------------------------------------------- /images/expectedFlow.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hennasingh/Android-Architecture-Components/9a9e29946752e5feeee9b8f11d123e15cf287ab3/images/expectedFlow.PNG -------------------------------------------------------------------------------- /images/intro_viewModel.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hennasingh/Android-Architecture-Components/9a9e29946752e5feeee9b8f11d123e15cf287ab3/images/intro_viewModel.PNG -------------------------------------------------------------------------------- /images/lifecycle_interfaces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hennasingh/Android-Architecture-Components/9a9e29946752e5feeee9b8f11d123e15cf287ab3/images/lifecycle_interfaces.png -------------------------------------------------------------------------------- /images/livedata_intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hennasingh/Android-Architecture-Components/9a9e29946752e5feeee9b8f11d123e15cf287ab3/images/livedata_intro.png -------------------------------------------------------------------------------- /images/observer_pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hennasingh/Android-Architecture-Components/9a9e29946752e5feeee9b8f11d123e15cf287ab3/images/observer_pattern.png -------------------------------------------------------------------------------- /images/onFinishAdd.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hennasingh/Android-Architecture-Components/9a9e29946752e5feeee9b8f11d123e15cf287ab3/images/onFinishAdd.PNG -------------------------------------------------------------------------------- /images/onHomeClick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hennasingh/Android-Architecture-Components/9a9e29946752e5feeee9b8f11d123e15cf287ab3/images/onHomeClick.png -------------------------------------------------------------------------------- /images/room_errordate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hennasingh/Android-Architecture-Components/9a9e29946752e5feeee9b8f11d123e15cf287ab3/images/room_errordate.png -------------------------------------------------------------------------------- /images/room_features.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hennasingh/Android-Architecture-Components/9a9e29946752e5feeee9b8f11d123e15cf287ab3/images/room_features.png -------------------------------------------------------------------------------- /images/runnable_UI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hennasingh/Android-Architecture-Components/9a9e29946752e5feeee9b8f11d123e15cf287ab3/images/runnable_UI.png -------------------------------------------------------------------------------- /images/runnable_thread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hennasingh/Android-Architecture-Components/9a9e29946752e5feeee9b8f11d123e15cf287ab3/images/runnable_thread.png -------------------------------------------------------------------------------- /images/screenstays_atupdate.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hennasingh/Android-Architecture-Components/9a9e29946752e5feeee9b8f11d123e15cf287ab3/images/screenstays_atupdate.PNG --------------------------------------------------------------------------------