Creating a ToDoList Android Application with Android Achitecture Components - PART 1
utopian-io·@ideba·
0.000 HBDCreating a ToDoList Android Application with Android Achitecture Components - PART 1
<html> <p>https://i.imgur.com/2Z1cfiJ.png</p> <p><strong>Repository</strong></p> <p>https://github.com/googlesamples/android-architecture-components/</p> <p><strong>What Will I Learn?</strong></p> <ul> <li>How to create a ToDo Listing Application using Room database.</li> </ul> <p><strong>Requirements</strong></p> <ul> <li>Java knowledge</li> <li>IDE for developing android applications(Android Studio or IntelliJ)</li> <li>An Android Emulator or device for testing</li> </ul> <p><strong>Other Resource Materials</strong></p> <ol> <li><a href="https://en.wikipedia.org/wiki/Software_design_pattern">Software Design Patterns</a></li> <li><a href="https://en.wikipedia.org/wiki/Singleton_pattern">The Singleton Pattern</a></li> <li>Google Room CodeLabs: https://codelabs.developers.google.com/codelabs/android-room-with-a-view/#11</li> <li><a href="https://www.sqlite.org/datatype3.html">Datatypes in SQLite</a></li> <li>https://guides.codepath.com/android/Room-Guide</li> </ol> <p><strong>Difficulty</strong></p> <ul> <li>Intermediate</li> </ul> <p><strong>Tutorial Duration</strong></p> <p><strong>30 - 35 Minutes</strong></p> <h1>TUTORIAL CONTENTS</h1> <p>In this tutorial, we are going to be creating a ToDo Listing application using the Android Architecture Components. According to <a href="https://developer.android.com/topic/libraries/architecture/">developer.android.com</a> Android Architecture Components is a collection of libraries that help you design robust, testable, and maintainable applications. Start with classes for managing your UI component lifecycle and handling data persistence. </p> <p>The purpose of Architecture Components is to provide guidance on app architecture, with libraries for common tasks like lifecycle management and data persistence. Architecture components help you structure your app in a way that is robust, testable, and maintainable with less boilerplate code. Architecture Components provide a simple, flexible, and practical approach that frees you from some common problems so you can focus on building great experiences. </p> <p>Since this Tutorial is in parts, of which this is the first part, we are going to be considering the Room Database for data persistence. Our Todo task will be saved locally to phone using Room.</p> <p><br></p> <p><strong>Why Room</strong></p> <p>The Room persistence library provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite. The library helps you create a cache of your app's data on a device that's running your app. This cache, which serves as your app's single source of truth, allows users to view a consistent copy of key information within your app, regardless of whether users have an Internet connection.</p> <p><br></p> <p><strong>Step 1 : Create a New Project</strong></p> <p>Open Android Studio and create a new project with an Empty activity.</p> <p>https://i.imgur.com/XbOuckh.png</p> <p><br></p> <p><strong>Step 2 : Update Gradle Files</strong></p> <p>You have to add the component libraries to your gradle files. Add the following code to your build.gradle (<strong>Module: app</strong>) file, below the dependencies block.</p> <p><br></p> <pre><code>implementation <strong>"android.arch.persistence.room:runtime:1.0.0"</strong><br> annotationProcessor <strong>"android.arch.persistence.room:compiler:1.0.0"</strong></code></pre> <p><br></p> <p>https://i.imgur.com/QpO0Igu.png</p> <p><strong>Step 3 : Create the MainActivity Layout</strong></p> <p>https://i.imgur.com/Khrfq9A.png</p> <p>layout source code here <a href="https://github.com/enyason/TodoApp_Android_Architecture_Components/blob/master/app/src/main/res/layout/activity_main.xml">github</a></p> <p><br></p> <p><strong>Step 4 : Create the Add Task Layout</strong></p> <p>https://i.imgur.com/G09lj0W.png</p> <p>layout source code here <a href="https://github.com/enyason/TodoApp_Android_Architecture_Components/blob/master/app/src/main/res/layout/activity_add_task.xml">github</a></p> <p><br></p> <p><strong>Step 5 : Create the Custom Row View Layout</strong></p> <p>https://i.imgur.com/Oex5j2q.png</p> <p>layout source code here <a href="https://github.com/enyason/TodoApp_Android_Architecture_Components/blob/master/app/src/main/res/layout/layout_row_item.xml">github</a></p> <p><br></p> <p><strong>Step 6 : Create An Entity</strong></p> <p>Create a class called Task that describes a task Entity.</p> <p><em>Here is the code</em></p> <pre><code>@Entity(tableName = <strong>"task"</strong>) <strong>public class </strong>Task {<br> @PrimaryKey(autoGenerate = <strong>true</strong>) <strong>private int id</strong>;<br> <strong>private </strong>String <strong>description</strong>;<br> <strong>private int priority</strong>;<br> @ColumnInfo(name = <strong>"updated_at"</strong>) <strong>private </strong>Date <strong>updatedAt</strong>;<br> <strong>public </strong>Task(String description, <strong>int </strong>priority, Date updatedAt) {<br> <strong>this</strong>.<strong>description </strong>= description;<br> <strong>this</strong>.<strong>priority </strong>= priority;<br> <strong>this</strong>.<strong>updatedAt </strong>= updatedAt;<br> }<br> <strong>public int </strong>getId() {<br> <strong>return id</strong>;<br> }<br> <strong>public </strong>String getDescription() {<br> <strong>return description</strong>;<br> }<br> <strong>public int </strong>getPriority() {<br> <strong>return priority</strong>;<br> }<br> <strong>public </strong>Date getUpdatedAt() {<br> <strong>return updatedAt</strong>;<br> }<br> <strong>public void </strong>setId(<strong>int </strong>id) {<br> <strong>this</strong>.<strong>id </strong>= id;<br> }<br> <strong>public void </strong>setDescription(String description) {<br> <strong>this</strong>.<strong>description </strong>= description;<br> }<br> <strong>public void </strong>setPriority(<strong>int </strong>priority) {<br> <strong>this</strong>.<strong>priority </strong>= priority;<br> }<br> <strong>public void </strong>setUpdatedAt(Date updatedAt) {<br> <strong>this</strong>.<strong>updatedAt </strong>= updatedAt;<br> }<br> }</code></pre> <p><br></p> <p>To make the Task class meaningful to a Room database, you need to annotate it. Annotations identify how each part of this class relates to an entry in the database. Room uses this information to generate code. </p> <ul> <li>Entity(tableName = "task")<br> Each Entity class represents an entity in a table. Annotate your class declaration to indicate that it's an entity. Specify the name of the table if you want it to be different from the name of the class.</li> <li>PrimaryKey<br> Every entity needs a primary key. To keep things simple, each Task acts as its own primary key.</li> <li>ColumnInfo(name = <strong>"updated_at"</strong>)<br> Specify the name of the column in the table if you want it to be different from the name of the member variable.</li> </ul> <p><strong>Step 6 : Create The DAO</strong></p> <p><strong>What is the DAO?</strong></p> <p>In the DAO (data access object), you specify SQL queries and associate them with method calls. The compiler checks the SQL and generates queries from convenience annotations for common queries, such as Insert. The DAO must be an interface or abstract class. By default, all queries must be executed on a separate thread. Room uses the DAO to create a clean API for your code. </p> <h2><strong>Implement the DAO</strong></h2> <p>The DAO for this tutorial provides queries for getting all the task, inserting a task, and deleting a task. </p> <ol> <li>Create a new Interface and call it TaskDataAccessObject. </li> <li>Annotate the class with DAO to identify it as a DAO class for Room. </li> <li>Declare a method to insert one task: <strong>void </strong>insertTask(Task task);</li> <li>Annotate the method with Insert. You don't have to provide any SQL! (There are also Delete and Update annotations for deleting and updating a row)</li> <li>Declare a method to delete a task: <strong>void </strong>deleteTask(Task task);</li> <li>Create a method to get all the task: loadAllTask();.<br> Have the method return a List of Task.<br> List<Task> loadAllTask();</li> <li>Annotate the method with the SQL query:<br> Query(<strong>"SELECT * FROM task ORDER BY priority"</strong>)</li> </ol> <p><br></p> <p>here is the code</p> <pre><code>@Dao <strong>public interface </strong>TaskDataAccessObject {<br> @Query(<strong>"SELECT * FROM task ORDER BY priority"</strong>) List<Task> loadAllTask(); <em>// returns a list of task object</em><br> <em> </em>@Insert <strong>void </strong>insertTask(Task task);<br> @Update(onConflict = OnConflictStrategy.<em><strong>REPLACE</strong></em>) <strong>void </strong>updateTask(Task task);<br> @Delete <strong>void </strong>deleteTask(Task task);<br> }</code></pre> <p><br></p> <p><br></p> <p><strong>Step 7 : Add a Room Database</strong></p> <p><strong>What is a Room database?</strong></p> <p>Room is a database layer on top of an SQLite database. Room takes care of mundane tasks that you used to handle with an SQLiteOpenHelper </p> <ul> <li>Room uses the DAO to issue queries to its database.</li> <li>By default, to avoid poor UI performance, Room doesn't allow you to issue database queries on the main thread. </li> <li>Room provides compile-time checks of SQLite statements.</li> <li>Your Room class must be abstract and extend RoomDatabase.</li> </ul> <h2><strong>Implement the Room database</strong></h2> <ol> <li>Create a public abstract class that extends RoomDatabase .</li> <li>Annotate the class to be a Room database, declare the entities that belong in the database and set the version number. Listing the entities will create tables in the database.</li> <li>Define the DAOs that work with the database. Provide an abstract "getter" method for each Dao.</li> <li>Make the class a singleton to prevent having multiple instances of the database opened at the same time.</li> <li>Add the code to get a database. This code uses Room's database builder to create a RoomDatabase object in the application context from the class.</li> </ol> <p>Here is the complete code for the class:</p> <pre><code>@Database(entities = {Task.<strong>class</strong>},version = 1,exportSchema = <strong>false</strong>) @TypeConverters(DateConverter.<strong>class</strong>) <strong>public abstract class </strong>AppDataBase <strong>extends </strong>RoomDatabase {<br> <strong>public static final </strong>String <em><strong>LOG_TAG </strong></em>= AppDataBase.<strong>class</strong>.getSimpleName(); <strong>public static final </strong>Object <em><strong>LOCK </strong></em>= <strong>new </strong>Object(); <strong>public static final </strong>String <em><strong>DATABASE_NAME </strong></em>= <strong>"todo_list"</strong>; <strong>private static </strong>AppDataBase <em>sInstance;</em><br> <strong>public static </strong>AppDataBase getsInstance(Context context){ <strong>if </strong>(<em>sInstance</em>== <strong>null</strong>) { <strong>synchronized </strong>(<em><strong>LOCK</strong></em>){ Log.<em>d</em>(<em><strong>LOG_TAG</strong></em>,<strong>"creating new database"</strong>);<br> <em>sInstance </em>= Room.<em>databaseBuilder</em>(context.getApplicationContext(), AppDataBase.<strong>class</strong>,AppDataBase.<em><strong>DATABASE_NAME</strong></em>) .allowMainThreadQueries() .build(); } }<br> Log.<em>d</em>(<em><strong>LOG_TAG</strong></em>,<strong>"getting the database instance"</strong>);<br> <strong>return </strong><em>sInstance</em>;<br> }<br> <strong>public abstract </strong>TaskDataAccessObject taskDao();<br> }</code></pre> <p><strong>Note: </strong></p> <p>The annotation TypeConverters(DateConverter.<strong>class</strong>) is used to provide room with a converter of the Date Type. This is beacause the Date DataType is not supported by SQlite. A class DateConverter is created to implement this conversion. </p> <p>here is the code:</p> <pre><code><strong>public class </strong>DateConverter {<br> @TypeConverter <strong>public static </strong>Date toDate(Long timeStamp) { <strong>return </strong>timeStamp == <strong>null </strong>? <strong>null </strong>: <strong>new </strong>Date(timeStamp); }<br> @TypeConverter <strong>public static </strong>Long toTimeStamp(Date date) { <strong>return </strong>date == <strong>null </strong>? <strong>null </strong>: date.getTime(); } }</code></pre> <p><br></p> <p><br></p> <p><strong>Step 8 : Add A List using a RecyclerView</strong></p> <p>Add a TodoListAdapter that extends RecyclerView.Adapter. </p> <pre><code><strong>public class </strong>ToDoListAdapter <strong>extends </strong>RecyclerView.Adapter<ToDoListAdapter.TaskViewHolder> {<br> <em>// Constant for date format</em>< <em> </em><strong>private static final </strong>String <em><strong>DATE_FORMAT </strong></em>= <strong>"dd/MM/yyy"</strong>;<br> <em>// Class variables for the List that holds task data and the Context</em> <em> </em><strong>private </strong>List<Task> <strong>mTaskEntries</strong>;<br> <strong>private </strong>Context <strong>mContext</strong>;<br> <em>// Date formatter</em> <em> </em><strong>private </strong>SimpleDateFormat <strong>dateFormat </strong>= <strong>new </strong>SimpleDateFormat(<em><strong>DATE_FORMAT</strong></em>, Locale.<em>getDefault</em>());<br> // the adapter constructor <strong>public </strong>ToDoListAdapter(Context context) {<br> <strong>mContext </strong>= context;<br> }<br> @Override <strong>public </strong>TaskViewHolder onCreateViewHolder(ViewGroup parent, <strong>int </strong>viewType) { <em>// Inflate the task_layout to a view</em><br> <em> </em>View view = LayoutInflater.<em>from</em>(<strong>mContext</strong>) .inflate(R.layout.<em><strong>layout_row_item</strong></em>, parent, <strong>false</strong>);<br> <strong>return new </strong>TaskViewHolder(view);<br> }<br> @Override <strong>public void </strong>onBindViewHolder(TaskViewHolder holder, <strong>int </strong>position) {<br> <em>// Determine the values of the wanted data</em><br> <em> </em>Task taskEntry = <strong>mTaskEntries</strong>.get(position); String description = taskEntry.getDescription(); <strong>int </strong>priority = taskEntry.getPriority(); String updatedAt = <strong>dateFormat</strong>.format(taskEntry.getUpdatedAt()); <em>// Toast.makeText(mContext,description,Toast.LENGTH_LONG).show();</em> <em> //Set values</em> <em> </em>holder.<strong>taskDescriptionView</strong>.setText(description); holder.<strong>updatedAtView</strong>.setText(updatedAt); <em>// Programmatically set the text and color for the priority //TextView</em> <em> </em>String priorityString = <strong>"" </strong>+ priority; <em>// converts int to String</em> <em> </em>holder.<strong>priorityView</strong>.setText(priorityString); GradientDrawable priorityCircle = (GradientDrawable) holder.<strong>priorityView</strong>.getBackground(); <em>// Get the appropriate background color based on the priority</em> <em> </em><strong>int </strong>priorityColor = getPriorityColor(priority); priorityCircle.setColor(priorityColor);<br> } <strong>private int </strong>getPriorityColor(<strong>int </strong>priority) {<br> <strong>int </strong>priorityColor = 0; <strong>switch </strong>(priority) { <strong>case </strong>1: priorityColor = ContextCompat.<em>getColor</em>(<strong>mContext</strong>, R.color.<em><strong>materialRed</strong></em>); <strong>break</strong>; <strong>case </strong>2: priorityColor = ContextCompat.<em>getColor</em>(<strong>mContext</strong>, R.color.<em><strong>materialOrange</strong></em>); <strong>break</strong>; <strong>case </strong>3: priorityColor = ContextCompat.<em>getColor</em>(<strong>mContext</strong>, R.color.<em><strong>materialYellow</strong></em>); <strong>break</strong>; <strong>default</strong>: <strong>break</strong>; } <strong>return </strong>priorityColor; } @Override <strong>public int </strong>getItemCount() { <strong>if </strong>(<strong>mTaskEntries </strong>== <strong>null</strong>) { <strong>return </strong>0; }<br> <strong>return mTaskEntries</strong>.size(); } <em>/**</em><br> <em> * When data changes, this method updates the list of taskEntries</em> <em> * and notifies the adapter to use the new values on it</em> <em> */</em> <em> </em><strong>public void </strong>setTasks(List<Task> taskEntries) { <strong>mTaskEntries </strong>= taskEntries; notifyDataSetChanged(); } <em>// Inner class for creating ViewHolders</em> <em> </em><strong>class </strong>TaskViewHolder <strong>extends </strong>RecyclerView.ViewHolder { <em>// Class variables for the task description and priority TextViews</em> <em> </em>TextView <strong>taskDescriptionView</strong>; TextView <strong>updatedAtView</strong>; TextView <strong>priorityView</strong>; <em> </em><strong>public </strong>TaskViewHolder(View itemView) { <strong>super</strong>(itemView); <strong>taskDescriptionView </strong>= itemView.findViewById(R.id.<em><strong>taskDescription</strong></em>); <strong>updatedAtView </strong>= itemView.findViewById(R.id.<em><strong>taskUpdatedAt</strong></em>); <strong>priorityView </strong>= itemView.findViewById(R.id.<em><strong>priorityTextView</strong></em>); } } }</code></pre> <p><br></p> <p><strong>Code Explanation</strong></p> <ul> <li>The TodoListAdapter is a custom adapter that populates the RecyclerView with the data model from the database.</li> <li>The Adapter class Constructor takes a Context as a paramenter. This context is to enabled the adapter determine the class where the adpter Object is being created. E.g The TodoListAdapter is instantiated in the MainActivity.</li> <li>In the OncreateViewHolder Method, we inflate the custom row view item layout we are using to display each entity and then return a new viewHolder object with this view as its parameter.</li> <li>In the OnbindViewHolder, we get each entity an set the atrributes to text the getPriority method is to return a color based on the task priority</li> <li>getCount method returns the size of the list</li> <li>the setTask methods provides the adapter wwith data from the database and then notify adapter for change</li> </ul> <p><br></p> <p>Add the RecyclerView in the onCreate() method of MainActivity.</p> <pre><code><strong>recyclerView </strong>= findViewById(R.id.<em><strong>recycler_view_main</strong></em>);<br> <strong>layoutManager </strong>= <strong>new </strong>LinearLayoutManager(<strong>this</strong>);<br> <strong>recyclerView</strong>.setLayoutManager(<strong>layoutManager</strong>);<br> <strong>toDoListAdapter </strong>= <strong>new </strong>ToDoListAdapter(<strong>this</strong>);<br> <strong>recyclerView</strong>.setAdapter(<strong>toDoListAdapter</strong>);<br> </code></pre> <p><br></p> <p><strong>Step 9 : Add a Task to the DataBase</strong></p> <p>In the Editor activity, set an onClick listener to the button add then include the following code:</p> <pre><code><strong>buttonAdd</strong>.setOnClickListener(<strong>new </strong>View.OnClickListener() { @Override <strong>public void </strong>onClick(View v) { String text = <strong>etTask</strong>.getTet().toString().trim(); <strong>int </strong>priority = getPriorityFromViews(); Date date = <strong>new </strong>Date(); Task task = <strong>new </strong>Task(text,priority,date); <strong>mdb</strong>.taskDao().insertTask(task); finish(); }<br> });</code></pre> <p><br></p> <p><strong>NOTE: </strong>Remember we said Room does not allow DB operations on the UI thread, but for simplicity of this first part, we allowed operations on the UI thread to allow Db operations without having to create a separate thread. To disable this restriction, include this line:</p> <p><em>In later part of this tutorial series, we will execute our db operations on a separate thread.</em></p> <pre><code>.allowMainThreadQueries()</code></pre> <p><br></p> <p>https://i.imgur.com/VhPoPWv.png</p> <p><br></p> <p><strong>Step 10 : Display Task in MainActivity</strong></p> <pre><code>@Override p<strong>rotected void </strong>onResume() { <strong>super</strong>.onResume(); <strong>toDoListAdapter</strong>.setTasks(<strong>appDataBase</strong>.taskDao().loadAllTask()); }</code></pre> <p><br></p> <p><strong>Code Explanation</strong></p> <p>In android activity life cycle, onResume method is called when an inactive activity becomes active. So in order to update our UI after adding a new task we have to call .loadAllTask from the OnResume method of the main ativity then notify the adapter to refresh the UI.</p> <p><br></p> <p><strong>Proof of Work Done</strong></p> <p>The Complete code can be found here <a href="https://github.com/enyason/TodoApp_Android_Architecture_Components">github</a></p> <p>https://github.com/enyason/TodoApp_Android_Architecture_Components</p> <p><br></p> <p><strong>Apllication Demo</strong></p> <p>https://youtu.be/CLwEU_UunvY</p> <p><br></p> <p><br></p> </html>