127 |
128 |
129 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/detail_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
19 |
20 |
23 |
24 |
33 |
34 |
47 |
48 |
61 |
62 |
75 |
76 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 |
16 | # Created by https://www.gitignore.io/api/kotlin,android,intellij,androidstudio
17 | # Edit at https://www.gitignore.io/?templates=kotlin,android,intellij,androidstudio
18 |
19 | ### Android ###
20 | # Built application files
21 | *.apk
22 | *.ap_
23 | *.aab
24 |
25 | # Files for the ART/Dalvik VM
26 | *.dex
27 |
28 | # Java class files
29 | *.class
30 |
31 | # Generated files
32 | bin/
33 | gen/
34 | out/
35 | release/
36 |
37 | # Gradle files
38 | .gradle/
39 | build/
40 |
41 | # Local configuration file (sdk path, etc)
42 | local.properties
43 |
44 | # Proguard folder generated by Eclipse
45 | proguard/
46 |
47 | # Log Files
48 | *.log
49 |
50 | # Android Studio Navigation editor temp files
51 | .navigation/
52 |
53 | # Android Studio captures folder
54 | captures/
55 |
56 | # IntelliJ
57 | *.iml
58 | .idea/workspace.xml
59 | .idea/tasks.xml
60 | .idea/gradle.xml
61 | .idea/assetWizardSettings.xml
62 | .idea/dictionaries
63 | .idea/libraries
64 | # Android Studio 3 in .gitignore file.
65 | .idea/caches
66 | .idea/modules.xml
67 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
68 | .idea/navEditor.xml
69 |
70 | # Keystore files
71 | # Uncomment the following lines if you do not want to check your keystore files in.
72 | #*.jks
73 | #*.keystore
74 |
75 | # External native build folder generated in Android Studio 2.2 and later
76 | .externalNativeBuild
77 |
78 | # Google Services (e.g. APIs or Firebase)
79 | # google-services.json
80 |
81 | # Freeline
82 | freeline.py
83 | freeline/
84 | freeline_project_description.json
85 |
86 | # fastlane
87 | fastlane/report.xml
88 | fastlane/Preview.html
89 | fastlane/screenshots
90 | fastlane/test_output
91 | fastlane/readme.md
92 |
93 | # Version control
94 | vcs.xml
95 |
96 | # lint
97 | lint/intermediates/
98 | lint/generated/
99 | lint/outputs/
100 | lint/tmp/
101 | # lint/reports/
102 |
103 | ### Android Patch ###
104 | gen-external-apklibs
105 | output.json
106 |
107 | # Replacement of .externalNativeBuild directories introduced
108 | # with Android Studio 3.5.
109 | .cxx/
110 |
111 | ### Intellij ###
112 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
113 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
114 |
115 | # User-specific stuff
116 | .idea/**/workspace.xml
117 | .idea/**/tasks.xml
118 | .idea/**/usage.statistics.xml
119 | .idea/**/dictionaries
120 | .idea/**/shelf
121 |
122 | # Generated files
123 | .idea/**/contentModel.xml
124 |
125 | # Sensitive or high-churn files
126 | .idea/**/dataSources/
127 | .idea/**/dataSources.ids
128 | .idea/**/dataSources.local.xml
129 | .idea/**/sqlDataSources.xml
130 | .idea/**/dynamic.xml
131 | .idea/**/uiDesigner.xml
132 | .idea/**/dbnavigator.xml
133 |
134 | # Gradle
135 | .idea/**/gradle.xml
136 | .idea/**/libraries
137 |
138 | # Gradle and Maven with auto-import
139 | # When using Gradle or Maven with auto-import, you should exclude module files,
140 | # since they will be recreated, and may cause churn. Uncomment if using
141 | # auto-import.
142 | # .idea/modules.xml
143 | # .idea/*.iml
144 | # .idea/modules
145 | # *.iml
146 | # *.ipr
147 |
148 | # CMake
149 | cmake-build-*/
150 |
151 | # Mongo Explorer plugin
152 | .idea/**/mongoSettings.xml
153 |
154 | # File-based project format
155 | *.iws
156 |
157 | # IntelliJ
158 |
159 | # mpeltonen/sbt-idea plugin
160 | .idea_modules/
161 |
162 | # JIRA plugin
163 | atlassian-ide-plugin.xml
164 |
165 | # Cursive Clojure plugin
166 | .idea/replstate.xml
167 |
168 | # Crashlytics plugin (for Android Studio and IntelliJ)
169 | com_crashlytics_export_strings.xml
170 | crashlytics.properties
171 | crashlytics-build.properties
172 | fabric.properties
173 |
174 | # Editor-based Rest Client
175 | .idea/httpRequests
176 |
177 | # Android studio 3.1+ serialized cache file
178 | .idea/caches/build_file_checksums.ser
179 |
180 | ### Intellij Patch ###
181 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
182 |
183 | # *.iml
184 | # modules.xml
185 | # .idea/misc.xml
186 | # *.ipr
187 |
188 | # Sonarlint plugin
189 | .idea/**/sonarlint/
190 |
191 | # SonarQube Plugin
192 | .idea/**/sonarIssues.xml
193 |
194 | # Markdown Navigator plugin
195 | .idea/**/markdown-navigator.xml
196 | .idea/**/markdown-navigator/
197 |
198 | ### Kotlin ###
199 | # Compiled class file
200 |
201 | # Log file
202 |
203 | # BlueJ files
204 | *.ctxt
205 |
206 | # Mobile Tools for Java (J2ME)
207 | .mtj.tmp/
208 |
209 | # Package Files #
210 | *.jar
211 | *.war
212 | *.nar
213 | *.ear
214 | *.zip
215 | *.tar.gz
216 | *.rar
217 |
218 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
219 | hs_err_pid*
220 |
221 | ### AndroidStudio ###
222 | # Covers files to be ignored for android development using Android Studio.
223 |
224 | # Built application files
225 |
226 | # Files for the ART/Dalvik VM
227 |
228 | # Java class files
229 |
230 | # Generated files
231 |
232 | # Gradle files
233 | .gradle
234 |
235 | # Signing files
236 | .signing/
237 |
238 | # Local configuration file (sdk path, etc)
239 |
240 | # Proguard folder generated by Eclipse
241 |
242 | # Log Files
243 |
244 | # Android Studio
245 | /*/build/
246 | /*/local.properties
247 | /*/out
248 | /*/*/build
249 | /*/*/production
250 | *.ipr
251 | *~
252 | *.swp
253 |
254 | # Android Patch
255 |
256 | # External native build folder generated in Android Studio 2.2 and later
257 |
258 | # NDK
259 | obj/
260 |
261 | # IntelliJ IDEA
262 | /out/
263 |
264 | # User-specific configurations
265 | .idea/caches/
266 | .idea/libraries/
267 | .idea/shelf/
268 | .idea/.name
269 | .idea/compiler.xml
270 | .idea/copyright/profiles_settings.xml
271 | .idea/encodings.xml
272 | .idea/misc.xml
273 | .idea/scopes/scope_settings.xml
274 | .idea/vcs.xml
275 | .idea/jsLibraryMappings.xml
276 | .idea/datasources.xml
277 | .idea/dataSources.ids
278 | .idea/sqlDataSources.xml
279 | .idea/dynamic.xml
280 | .idea/uiDesigner.xml
281 |
282 | # OS-specific files
283 | .DS_Store
284 | .DS_Store?
285 | ._*
286 | .Spotlight-V100
287 | .Trashes
288 | ehthumbs.db
289 | Thumbs.db
290 |
291 | # Legacy Eclipse project files
292 | .classpath
293 | .project
294 | .cproject
295 | .settings/
296 |
297 | # Mobile Tools for Java (J2ME)
298 |
299 | # Package Files #
300 |
301 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
302 |
303 | ## Plugin-specific files:
304 |
305 | # mpeltonen/sbt-idea plugin
306 |
307 | # JIRA plugin
308 |
309 | # Mongo Explorer plugin
310 | .idea/mongoSettings.xml
311 |
312 | # Crashlytics plugin (for Android Studio and IntelliJ)
313 |
314 | ### AndroidStudio Patch ###
315 |
316 | !/gradle/wrapper/gradle-wrapper.jar
317 |
318 | # End of https://www.gitignore.io/api/kotlin,android,intellij,androidstudio
319 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android App Architecture in The App Factory
2 | #### Authors
3 | - Marshall Ladd
4 | -
5 | ### The MVVM Design Pattern
6 |
7 | #### Contents
8 | - [Introduction](#introduction)
9 | - [How to Use This Guide](#how-to-use-this-guide)
10 | - [MVVM](#mvvm)
11 | - [What is MVVM?](#what-is-mvvm)
12 | - [How it Works](#how-it-works)
13 | - [Room Database](#room-database)
14 | - [Repositories](#repositories)
15 | - [Code Style Guide](#code-style-guide)
16 | - [Other Tools](#other-tools)
17 | - [Navigation](#navigation)
18 | - [Splash Screens](#splash-screens)
19 | - [Retrofit2](#retrofit2)
20 | - [Timber](#timber)
21 | - [Material Components](#material-components)
22 | - [Instructions](#instructions)
23 | - [Step By Step](#step-by-step)
24 | - [How to Learn an App](#how-to-learn-an-app)
25 | - [Other](#other)
26 | - [Disclaimer](#disclaimer)
27 |
28 | # Introduction
29 | This project is to provide a simple example app for Android Developers
30 | at The App Factory to reference when starting a new app, or adding new
31 | features to an existing app.
32 |
33 | The architecture pattern shown here and to be used in apps, whenever
34 | possible, is Model-View-ViewModel, or MVVM. This is the pattern
35 | recommended and used by Google.
36 |
37 | ### App Features
38 | - Connects to a remote database -
39 | [documentation](http://dummy.restapiexample.com/)
40 | - Downloads JSON(Employee) data from remote database
41 | - Stores data in Room Database in app
42 | - Displays list of Employees from database
43 | - Allows searching of database
44 | - Select a single Employee and show details of
45 |
46 | ### Language and IDE
47 | This project was written using Kotlin 1.3.60 in Android Studio 3.5.2.
48 | All Android apps in the App Factory will be written in Kotlin.
49 |
50 | ### Prerequisites
51 | This project assumes a base knowledge of Kotlin and Android, such as
52 | Activities, Fragments, RecyclerViews, and the Manifest.
53 |
54 | ## How to Use This Guide
55 | This README has a general summary of the design principles used in this
56 | app, using code snippets to show simplified examples to accompany the
57 | explanations. You should reference the code in the app for the complete
58 | implementation of classes.
59 |
60 | Further down the guide, there is a [Step By Step](#step-by-step) summary
61 | of how I built this app. I will briefly explain what I implemented, in
62 | the order I implemented it.
63 |
64 | The Master branch will always have the latest full, approved, running,
65 | and commented code.
66 |
67 | After that, there is a section for developers, new and experienced, who
68 | are asked to jump on a project that's already going. This section will
69 | walk you through my steps on how I approach someone else's code. The
70 | steps I will go over are applicable to any and all Android apps, not
71 | just one's that use MVVM. I will be using this app as an example, and
72 | pretending we had no comments while I do so.
73 |
74 | # MVVM
75 |
76 | ## What is MVVM?
77 | MVVM is a flexible guide and set of libraries used to standardize an
78 | app's architecture. What this means is that your code is split into
79 | distinct components that hold specific parts of code in specific areas.
80 | These parts then interact with each other in a set order. These parts
81 | are:
82 |
83 | #### Model
84 | The Model consists of three parts
85 |
86 | - Database Class
87 | - DAO Interfaces
88 | - Data Model Classes
89 |
90 | In this app, our local database is Room. In other apps, Firebase may be
91 | the database. The flexibility of MVVM allows for these differences, and
92 | does not specify **what** to use, just **where** to put the code.
93 |
94 | #### View
95 | The View is the UI presentation logic. It consists of
96 |
97 | - Activities
98 | - Fragments
99 | - Adapters
100 | - XML
101 |
102 | The View should only be responsible for displaying values and state.
103 | Another way to say that is you should not make database requests and/or
104 | network requests in the View. These will be done elsewhere. This means
105 | the Views don't care where the data comes from or how it gets there, it
106 | just shows whatever data there is. This way, if a change needs to be
107 | made to the database, the View should not need to be changed at all.
108 |
109 | #### View Model
110 | The View Model is responsible for interacting with the Database and
111 | coordinating between any remote sources. It is then responsible for
112 | representing the state of the data to the View. This coordination
113 | creates what is known as a **single source of truth** and exposes it to
114 | the Views. This way, you can be as sure as you can be, that what is
115 | being shown to the user is accurate, and where it came from.
116 |
117 | ViewModels provided by the Jetpack components are also Lifecycle aware,
118 | and can survive configuration changes. This helps:
119 | - Prevent memory leaks
120 | - Prevent errant network calls
121 | - Prevent null pointer exceptions due to UI changes
122 | - Solves the issue of what happens when you rotate an app
123 |
124 | ## How it works
125 | As mentioned before MVVM breaks the app into components and they
126 | interact in a certain way. This interaction follows the pattern:
127 | 1. The View subscribes to a LiveData from a ViewModel
128 | 2. The ViewModel connects to a repository
129 | 3. The Repository connects to databases, both remote and local, if there
130 | are both
131 | 4. The Repository returns the requested data to the the ViewModel
132 | 5. The ViewModel formats the data and exposes it through a LiveData
133 | 6. The View's subscriptions are notified of any changes through the
134 | LIveData, and updates the UI to match
135 |
136 | #### LiveData
137 | LiveData was mentioned a few times, so what is it? LiveData is a wrapper
138 | class for data objects. It is observable from a View. Being observable
139 | is a way to automatically call update UI logic, anytime the data is
140 | changed. There's more to it than all that, but those are the very
141 | basics. If they don't make sense now, they will after you use them.
142 |
143 | ### Basic MVVM Example
144 | Inside of a Fragment such as ListFragment, get an instance of the
145 | ViewModel needed. Here we are using a shortcut method, thanks to a KTX
146 | library.
147 |
148 | ```kotlin
149 | private val listViewModel by viewModels()
150 | ```
151 |
152 | The ViewModel will have a Function or Object that triggers background
153 | database and network requests, and immediately returns a LiveData Object
154 | to be observed.
155 |
156 | ```kotlin
157 | fun getAllEmployees(): LiveData> {
158 | // Database and or network logic happens
159 | // return LiveData>
160 | }
161 | ```
162 |
163 | Then, back in the Fragment, you can call to this method, and observe the
164 | returned LiveData and update your UI with the contained data. When the
165 | background database or network requests finish, they post their updates
166 | to this Object, triggering a UI update.
167 |
168 | ```kotlin
169 | listViewModel.getAllEmployees().observe(this) { employeeList ->
170 | recyclerViewAdapter.submitList(employeeList)
171 | // other operations related to getting new data
172 | }
173 | ```
174 |
175 | ## Room Database
176 | This app uses the Room Database library, provided by Google as part of
177 | Android Jetpack. Room is a wrapper for Android's SQLite DB. Essentially,
178 | it operates like Retrofit, but for database calls. It relies heavily on
179 | annotations to generate code for you. Room also natively supports
180 | LiveData.
181 |
182 | Documentation can be found [here](https://developer.android.com/training/data-storage/room).
183 |
184 | A tutorial on Room, provided by Google, can be found
185 | [here](https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#0).
186 |
187 | ### Basic Room Example
188 | Room consist of three main parts
189 | - The Database Class
190 | - Data Access Object Interfaces
191 | - Annotated Data Classes
192 |
193 | First, create a Data class to store in Room and add the **@Entity** and
194 | **@PrimaryKey** annotations where needed.
195 |
196 | ```kotlin
197 | @Entity(tableName = "employee_table")
198 | data class Employee(@PrimaryKey val id: Int, val name: String)
199 | ```
200 |
201 | Next, create a Data Access Object Interface or DAO. This is where you
202 | will define all of your database CRUD actions. Note all the annotations
203 | and that this is an interface, and you do not write any function bodies.
204 | Room will write these for you.
205 |
206 | ```kotlin
207 | @Dao
208 | interface EmployeeDAO {
209 |
210 | @Insert(onConflict = OnConflictStrategy.REPLACE)
211 | fun insertEmployee(employee: Employee)
212 |
213 | @Delete
214 | fun deleteEmployee(employee: Employee)
215 |
216 | @Query("SELECT * FROM employee_table")
217 | fun getAllEmployeesLiveData(): LiveData>
218 |
219 | @Query("SELECT * FROM employee_table WHERE id = :employeeId")
220 | fun getEmployeeByIdLiveData(employeeId: Int): LiveData
221 | }
222 | ```
223 |
224 | Finally, we create our database class by extending RoomDatabase and
225 | marking it with some more annotations.
226 |
227 | ```kotlin
228 | @Database(entities = [Employee::class], version = 1, exportSchema = false)
229 | abstract class ExampleRoomDB : RoomDatabase() {
230 |
231 | abstract fun getEmployeeDao(): EmployeeDAO
232 |
233 | companion object {
234 | private var INSTANCE: ExampleRoomDB? = null
235 |
236 | fun getDatabase(context: Context) = INSTANCE ?: kotlin.run {
237 | Room.databaseBuilder(
238 | context.applicationContext,
239 | ExampleRoomDB::class.java,
240 | ROOM_DB_NAME
241 | )
242 | .fallbackToDestructiveMigration()
243 | .build()
244 | }
245 | }
246 | }
247 | ```
248 |
249 | In the above code, you tell Room which classes are going to be stored in
250 | the DB, what version the DB is on, and another flag that isn't important
251 | now. Keep it false. We also define an abstract function that return the
252 | DAO we created earlier. Do this for all DAOs in your app.
253 |
254 | When using Room, we also need to make a slight change to our ViewModels.
255 | If the ViewModel needs to access the Room Database, either directly or
256 | through a repository, it will need to extend **AndroidViewModel**
257 | instead of the normal **ViewModel**.
258 |
259 | ##### ViewModel Used WITHOUT Room
260 | ```kotlin
261 | class DetailViewModel : ViewModel() {
262 | // ViewModel methods here
263 | }
264 | ```
265 | ##### ViewModel Used WITH Room
266 | ```kotlin
267 | class DetailViewModel(application: Application) : AndroidViewModel(application) {
268 | // ViewModel methods here
269 | }
270 | ```
271 |
272 | At this point, you can get an instance of your DB and get an instance of
273 | the DAO, and start making calls to and storing data in your DB. These
274 | calls could be made directly in the Fragment, but that would be mixing
275 | components in MVVM(calling DB logic from the View).
276 |
277 | We could call the DAO methods from the ViewModel and expose the results
278 | to the UI through that. This is an acceptable approach, and a relatively
279 | good one. However, there is a better way.
280 |
281 | ## Repositories
282 | Repositories, while not required and aren't an official component of
283 | MVVM, they are considered a good practice. Especially when you have data
284 | being stored locally and data available on a remote API. The repository
285 | provides what is known as a **single source of truth**. More simply put,
286 | when data is coming possibly coming from multiple locations, the
287 | repository decides what is valid, what to show, where to call, and when
288 | to make those calls.
289 |
290 | ### Basic Repository Example
291 | Let's assume that our app has a Room Database completely set up,
292 | Retrofit for API calls, and an AndroidViewModel as described in previous
293 | sections. Create a new repository class. In this class, get an instance
294 | of the DAO and RetroFit interfaces.
295 |
296 | ```kotlin
297 | class EmployeeRepository private constructor(application: Application) {
298 | private val employeeDAO: EmployeeDAO = ExampleRoomDB.getDatabase(application).getEmployeeDao()
299 | private val employeeCalls = RetroFitInstance.getInstance().create(EmployeeEndpoints::class.java)
300 | }
301 | ```
302 |
303 | Suppose we want to show the entire list of Employees. We need to check
304 | the API for current data, store this data, and show it to the user. But
305 | what if there is an update to the data, or what if the network is down.
306 | This is where the repository comes into play.
307 |
308 | You will create a method that calls to the API **AND** to the local
309 | database.
310 |
311 | ```kotlin
312 | suspend fun getAllEmployeesLiveData(): LiveData> {
313 | return employeeDAO.getAllEmployeesLiveData().also {
314 | try {
315 | val employeeList = employeeCalls.getEmployeeList()
316 | employeeList.forEach {
317 | insertEmployee(it)
318 | }
319 | } catch (exception: Throwable) {
320 | Timber.e(exception)
321 | }
322 | }
323 | }
324 | ```
325 |
326 | The data in the database will be shown to the user immediately, through
327 | LiveData, while, in the background, a request to the API for new and
328 | current data has been made. When that request comes back, each result is
329 | inserted into the database. Since we returned a LiveData, any updates to
330 | the Database will notify the LiveData, which will update itself with the
331 | current information.
332 |
333 | Finally, update our ViewModel to use our repository, instead of calling
334 | to the API or to the database directly.
335 |
336 | ```kotlin
337 | class ListViewModel(application: Application) : AndroidViewModel(application) {
338 |
339 | private val employeeRepository = EmployeeRepository.getInstance(application)
340 |
341 | val employeeListLiveData: LiveData> = liveData(Dispatchers.IO) {
342 | emitSource(employeeRepository.getAllEmployeesLiveData())
343 | }
344 | }
345 | ```
346 |
347 | Now your UI should update with the most current data, and also be able
348 | to show data that was stored if there is no network.
349 |
350 | If you follow the steps in this guide, you'll notice that once we set up
351 | our Fragment, and subscribed to a LiveData object from a ViewModel, we
352 | never had to change anything in our Fragment. It just kept on working.
353 | This is what MVVM does for you. When you need to change a component, it
354 | doesn't affect other components.
355 |
356 | For example, it's a week before this app is deploying and we decided to
357 | change our Database from a REST API, like this app does now, to
358 | Firebase's Firestore. What would need to be changed? Firebase provides
359 | it's own local backup, so we won't need Room anymore. Firebase also has
360 | it's own calls to the API, so we can't use Retrofit calls either.
361 | Thankfully, all of that is in one place, the repository. We just change
362 | the functions in the repository to use Firebase, remove the unneeded
363 | code, and everything else remains the same. As long as we keep returning
364 | LiveData, the UI won't care where it came from, just as that it gets
365 | there.
366 |
367 | ## Final Thoughts on MVVM
368 | That's it. This app is ready to be released or expanded on. You could
369 | add more endpoints, add new database operations, add new screens, add
370 | new data classes, add whole new features, whatever. Just follow the
371 | patterns seen here when doing so as best you can.
372 |
373 | This guide is meant to be a general summary of the MVVM concept and give
374 | you an idea of how to implement it in your app. It is not a perfect
375 | example. For instance, MVVM recommends the use of DataBinding or
376 | ViewBinding and I did not use any of it in this app. This app is meant
377 | to be a starting point. Any improvements and further refinement to the
378 | design should be encouraged, however, deviations from the main design
379 | pattern should be kept to a minimum whenever possible.
380 |
381 | # Code Style Guide
382 | The following naming and style conventions should be used across all
383 | projects to aid in readability of code from one project to another.
384 |
385 | ##### Variables
386 | - Standard variables shall be named using camelCaseNotation.
387 | - Hungarian notation(mVarName, sVarname, etc.) is **NOT** to be used.
388 |
389 | ```kotlin
390 | // Do this
391 | val thisIsAVariable = "Some String value"
392 | // Don't do this
393 | val mThisIsAMemberVariable = 42
394 | ```
395 |
396 | ##### Constants
397 | - Constants shall be declared in all caps.
398 |
399 | ```kotlin
400 | const val THIS_IS_A_URL_CONSTANT = "https:\\www.google.com"
401 | ```
402 |
403 | ##### Functions
404 | - Functions and their parameters will be named using camelCaseNotation.
405 | - If there is no return value, omit return value in signature.
406 |
407 | ```kotlin
408 | // A function with no parameters or return value
409 | fun aBoringFunction() {
410 | // Does stuff
411 | }
412 | // A function with parameters and a return type
413 | fun doSomeMath(inputA: Int, inputB: Int): Int {
414 | return inputA + inputB
415 | }
416 | // This is wrong.
417 | fun NotInCamelCase(): Unit {
418 | println("I return nothing and should not have Unit in my signature.")
419 | }
420 | ```
421 |
422 | ##### Classes
423 | - Classes shall be named in FirstLetterCapsCamelCase.
424 | - When possible, data classes should be used over standard classes.
425 |
426 | ```kotlin
427 | data class ExampleDataClass(
428 | val id: Int,
429 | val exampleField: String
430 | )
431 | ```
432 |
433 | ##### ktlint
434 | ktlint is a linter and formatter for Kotlin code. What that means is
435 | that it will go through your code and look for formatting errors and fix
436 | them for you. Things like adding to many indents or spaces before or
437 | after lines, removing unused imports, and ensuring the guidelines above
438 | are being followed are taken care of for you by running a simple script.
439 |
440 | Installation and usage instructions can be found here:
441 | - [ktlint](https://ktlint.github.io/)
442 |
443 | ##### View id's
444 | - View id's shall be named using camelCaseNotation
445 | - id's shall be formatted viewTypeThenFunction
446 |
447 | ```xml
448 |
450 |
452 | ```
453 |
454 | # Other Tools
455 |
456 | ## Navigation
457 | This app uses the Navigation Component from Jetpack. More can be read
458 | about it from the [official documentation](https://developer.android.com/guide/navigation/navigation-getting-started) from Google, as well a
459 | [simple tutorial](https://codelabs.developers.google.com/codelabs/android-navigation/#0)
460 | to show you the basics.
461 |
462 | This app does not use the SafeArgs plugin as it was meant to be very
463 | simple. This plugin, described in the tutorial and documentation should
464 | be used when possible.
465 |
466 | ## Splash Screens
467 | This app shows an example on how to properly implement a Splash(startup)
468 | Screen. A splash screen is a good practice as it gives the user
469 | something to look at while the app is loading, instead of a plain, white
470 | screen. This method is from an
471 | [article](https://www.bignerdranch.com/blog/splash-screens-the-right-way/)
472 | by BigNerdRanch. It is written in Java and from 2015, but still applies.
473 | The Java translates directly to Kotlin and the rest stays basically the
474 | same.
475 |
476 | ###### Important Note
477 | If your app implements a login screen, the SplashActivity is an ideal
478 | place to execute logic to verify if a User is still logged in or not,
479 | and send them to the correct portion of the app, based on the result.
480 |
481 | ## Retrofit2
482 | Retrofit2 is a library, written by Jake Wharton, for performing RESTful
483 | API requests on the background, simply and without having to write any
484 | real code. It accomplishes this through annotation processing. Think
485 | Room DAO interface methods, but for network requests. This is the
486 | industry standard way to do all of your RESTful API calls.
487 |
488 | A recent update added support for Kotlin coroutine suspend functions,
489 | making Retrofit2 easier to use than ever.
490 |
491 | More info and documentation about Retrofit2 can be found at the
492 | following locations:
493 | - [Official site](https://square.github.io/retrofit/)
494 | - [Tutorial with suspend functions](https://proandroiddev.com/suspend-what-youre-doing-retrofit-has-now-coroutines-support-c65bd09ba067)
495 |
496 | ## Timber
497 | Timber is another library written by Jake Wharton, that makes writing
498 | log messages easier, and helps prevent them from making it into a
499 | production release. As a good practice, all log messages lower than the
500 | Warn level should be removed before being released to production. Timber
501 | does this for us.
502 |
503 | More information can be found here:
504 | - [Timber Tutorial](https://medium.com/mindorks/better-logging-in-android-using-timber-72e40cc2293d)
505 |
506 | ## Material Components
507 | Material Components are a set of libraries that allow you to quickly and
508 | consistently theme your apps. You'll notice that in all the Layout XML
509 | files, size, color, font size, and attributes like that are never
510 | directly set. All of them rely on MaterialComponent styles and themes.
511 |
512 | This app has a very basic implementation of it, and is only using the
513 | color portion and part of the typography tools. There is a whole shape
514 | component as well.
515 |
516 | The Material design team has provided an app for you to run from Android
517 | Studio where you can customise various XML files to modify the apps
518 | theme. You can then run the app and see what all the components would
519 | look like using that theme. You can then copy/paste the theme files from
520 | that app into any other app, and you will have a new theme and look for
521 | your app.
522 |
523 | More information and that app can be found here:
524 | - [Build a Material Theme App](https://material.io/resources/build-a-material-theme/#how-to-using-android-studio)
525 | - [Material tutorial](https://medium.com/over-engineering/setting-up-a-material-components-theme-for-android-fbf7774da739)
526 |
527 | # Instructions
528 |
529 | ## Step By Step
530 | While an attempt was made to do everything in order, document
531 | everything, and get it all correct, there may be errors or things
532 | missing from this guide. If you find something, please refer to
533 | [Submit Changes](#submit-changes) for info on how you can submit an
534 | update or fix for it.
535 |
536 | Each step in this guide has an associated branch in this repository.
537 | Checking out each branch will show you how the app would look after
538 | finishing the step. Large portions of the app may be refactored from
539 | step to step. Refer to the *master* branch for the final implementation.
540 |
541 | 0. Create a new project in Android Studio.
542 | - Min API level 21 is the **absolute** minimum we should select here
543 | - API level 23 would be preferred
544 | - Don't create a default Activity yet
545 | 1. Dependencies and Activities
546 | - Add known dependencies and settings to gradle
547 | - In this instance, it is best to use the gradle from the Master as
548 | your example, and not the one in this steps branch
549 | - Add Splash Activity to project
550 | - Do not generate XML/UI for this
551 | - Set this as the Launcher Activity
552 | - Add Main Activity
553 | - Do generate XML/UI for this
554 | - Do not make this the Launcher Activity
555 | - Implement [splash screen](#splash-screens)
556 | 2. Fragments
557 | - Add Start, List, Search, and Detail Fragment
558 | - Start Fragment - select Fragment (blank) from menu
559 | - All others - select Fragment (with ViewModel) from menu
560 | 3. Navigation
561 | - Implement basic [navigation](#navigation) for the app
562 | - NavGraph
563 | - Connect MainActivity
564 |
565 | ##### RUN THE APP
566 | The app should run at this point. It will load the MainActivity, which
567 | should be displaying the StartFragment. Unless you update the UI and
568 | connect buttons, you'll only be able to see StartFragment. This will
569 | happen in the next step.
570 |
571 | If this is not happening, troubleshoot until it is. **DO NOT** continue
572 | until this is working.
573 |
574 | 4. UI
575 | - Design the app UI in XML
576 | - Add Buttons, RecyclerView, etc. to all screens
577 | 5. Finish Navigation
578 | - Navigate between Fragments using the Navigation Component
579 | - Does not need to pass data between Fragments yet
580 |
581 | ##### RUN THE APP
582 | Make sure the app behaves as expected and navigates between the screens
583 | correctly. If it doesn't, troubleshoot until it does, then move on.
584 |
585 | 6. Data Classes
586 | - Create the Employee Data Class
587 | - Employee in this example has annotations in places
588 | - Annotations are in preparation for Retrofit
589 | 7. RecyclerViewAdapter
590 | - Create the RecyclerViewAdapter for Employees
591 | - Implement it in the list fragment
592 | - Pass in a static test list of data to make sure it works
593 |
594 | ##### RUN THE APP
595 |
596 | 8. Retrofit, LiveData, and ViewModels
597 | - Create Retrofit Interface
598 | - Make function that returns LiveData> in ViewModel
599 | - Subscribe to that LiveData from the Fragment
600 | - Back in the ViewModel make the network call using retrofit inside
601 | of the function and emit its return value through the LiveData
602 | - Update RecyclerViewAdapter to receive its list from the LiveData
603 | Observer
604 |
605 | ##### RUN THE APP
606 |
607 | 9. Room and Repositories
608 | - Implement Room according to the examples and documentation here
609 | - Refactor Room and ViewModel to use the repository pattern
610 |
611 | ##### RUN THE APP
612 | Congrats, you now have a basic app that connects to a remote API,
613 | downloads and stores data, then displays that data. It also has offline
614 | backup and a very simple search function.
615 |
616 | ## How to Learn an App
617 | In this section, I will describe the approach I take when looking at any
618 | Android code for the first time. Following these steps will show you how
619 | any app is structured, how it navigates between each screen, and where
620 | any data is coming from and being stored. That's about 80-90% of what an
621 | app is. Even better, you'll be able to do this, even if there is no
622 | documentation or comments anywhere.
623 |
624 | First thing I like to look at when I approach an app is the app's gradle
625 | file. In here you can see what version of Android they are targeting.
626 | Further down the file, you'll see what dependencies are being used and
627 | if they are the current version or not. This can tell you a lot about an
628 | app. Like if you were told it connects to a remote API and you don't see
629 | anything about Retrofit or Firebase, you'll know to start worrying.
630 |
631 | Next, I'll open the manifest.xml file. The manifest essentially
632 | describes the app to the operating system, so lets see what it's saying.
633 |
634 | ```xml
635 |
636 |
639 |
640 |
641 |
642 |
643 | ```
644 | This first section tells us what the package name of the app is. This
645 | name must be unique across every app in the entire Play Store. In this
646 | app, the package name is "ladd.marshall.androidmvvmexample". This is an
647 | incorrect package name for an App Factory app and should be fixed.
648 |
649 | ```xml
650 |
651 | ```
652 | This block tells the OS which permissions the app will be using, such as
653 | Internet, bluetooth, or location.
654 |
655 | ```xml
656 |
666 |
667 |
668 |
669 | ```
670 | This tells us what the Application is, what it should be called,how it
671 | should look, and if we have any special configurations we want to make.
672 |
673 | The important line for us right now is the line that says name. If this
674 | is not in your app, move on to the next section. Your app is using
675 | default application settings, and this is ok and normal.
676 |
677 | So what does this line mean to us? It points to a file somewhere in the
678 | Project that is overriding the Application class. To quickly navigate to
679 | that file, click on the value ".BaseApplication" then hit command + B on
680 | a Mac, and you will be taken straight to that file. This shortcut will
681 | be used often.
682 |
683 | Our Application file is pretty boring. It is only setting up the Timber
684 | logging dependency. In other apps, say one that was using the RealmDB
685 | for its database, this is where you would init that. Let's move on.
686 |
687 | ```xml
688 |
689 |
691 |
692 |
693 |
694 |
695 |
696 |
697 | ```
698 | Now we are getting somewhere. Here we see all the Activities that are in
699 | our app. Here we have a MainActivity and a SplashActivity declared.
700 | Remember, Activities are what host our app's UI and logic.
701 |
702 | The key thing to look for are the lines in-between the intent-filter
703 | flags. The tags MAIN and LAUNCHER we see here tell us that the
704 | SplashActivity is the first Activity that will load when the app is
705 | started up. This is huge information. Let's see what our first activity
706 | does. Click on its name value, and hit command + B to be taken to the
707 | SplashActivity implementation.
708 |
709 | Remember we are looking at this and imagining that there are no
710 | comments. Looking at this activity, it looks like it loads up, makes an
711 | Intent with MainActivity as an argument, and starts that Activity up. So
712 | let's see what happens in there. Click MainActivity and then command +
713 | B.
714 |
715 | We should now be in the MainActivity. Here we can see how the app
716 | navigates around and what is shown to the User. This app uses the
717 | navigation component, so we'll need to go to the nav graph to see what
718 | the first Fragment is. In other apps, you might see the Fragment loaded
719 | here directly, in others, all the logic may reside inside of Activities.
720 |
721 | Following our app's logic, we can look at the nav graph and see that
722 | StartFragment is our first Fragment shown to the User.
723 |
724 | We now know how the app is configured based on our Application file,
725 | what the first Activity is, where that Activity leads, what that new
726 | Activity does, and finally, what the first screen is that the user will
727 | interact with is, and where it goes from there. That's a lot of
728 | information already, but we can do better.
729 |
730 | So let's look at the first Fragment, StartFragment. It's a simple
731 | Fragment that navigates between screens. Let's look at what happens when
732 | we navigate to the ListFragment.
733 |
734 | Open up ListFragment. We can see it is using a ViewModel and a
735 | RecyclerView. If we want to get a quick look at the RecyclerViewAdapter,
736 | select it in the code and hit command + B again. You'll be taken
737 | directly to the implementation. Back to the ListFragment. In it we see
738 | it is getting data from the ViewModel and passing it to the Adapter. But
739 | where is this data coming from? Select where we called
740 | employeeListLiveData and command + B again to be taken to the funtion
741 | inside the ViewModel.
742 |
743 | In the ViewModel, we see that the LiveData is emiting a value from a
744 | repository. Let's select that method and command + B again. We are then
745 | taken to a repository class that is making calls to our Room DB and
746 | making calls to our Remote DB using Retrofit.
747 |
748 | To learn more about the Room or Retrofit methods and values, we could
749 | select them and hit command + B to be taken to our DAO and Retrofit
750 | Interfaces.
751 |
752 | Now we know how and where our app gets its data from, on top of all of
753 | the things we had learned before, like how the app navigates from screen
754 | to screen. And those are the most important details of how any app
755 | works. Just follow the code from it's entry point in the manifest until
756 | you hit a dead end and can't go any further in the app, then back up and
757 | try again, going a different way until it dead ends. Keep doing this
758 | over and over until you've explored the whole app, or just the section
759 | you need to be working on and you'll be up to speed in no time.
760 |
761 | # Other
762 |
763 | ## Disclaimer
764 | This app is meant to show architecture examples. It is **NOT** meant to
765 | look good, or to show how to design a UI. There are some good practices
766 | shown, such as using Material Theme and how to implement a proper splash
767 | screen, but overall, **DO NOT MAKE YOUR APP LOOK LIKE THIS**.
768 |
769 | This is a very basic example and is **NOT** meant to show how to do
770 | everything, just a general structure to follow. For a more advanced
771 | example of MVVM implemented in a larger app, reference the Unico Reports
772 | Android App.
773 |
774 | This app is **NOT** to be treated as the absolute only way to do things.
775 | For instance, this app uses Room Database and Retrofit. If using
776 | something else, such as Firebase, this pattern can still be followed.
777 |
--------------------------------------------------------------------------------