14 | * This avoids a common problem with events: on configuration change (like rotation) an update
15 | * can be emitted if the observer is active. This LiveData only calls the observable if there's an
16 | * explicit call to setValue() or call().
17 | *
18 | * Note that only one observer is going to be notified of changes.
19 | */
20 | class SingleLiveEvent : MutableLiveData() {
21 |
22 | private val mPending = AtomicBoolean(false)
23 |
24 | @MainThread
25 | override fun observe(owner: LifecycleOwner, observer: Observer) {
26 | observe(owner, observer::onChanged)
27 | }
28 |
29 | @MainThread
30 | fun observe(owner: LifecycleOwner, observer: (T?) -> Unit) {
31 | super.observe(owner, Observer { t ->
32 | if (mPending.compareAndSet(true, false)) {
33 | observer(t)
34 | }
35 | })
36 | }
37 |
38 | @MainThread
39 | override fun setValue(t: T?) {
40 | mPending.set(true)
41 | super.setValue(t)
42 | }
43 |
44 | /**
45 | * Used for cases where T is Void, to make calls cleaner.
46 | */
47 | @MainThread
48 | fun call() {
49 | value = null
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/azadev/android/architecture/feat/part4/Part4ListAdapter.kt:
--------------------------------------------------------------------------------
1 | package azadev.android.architecture.feat.part4
2 |
3 | import android.support.v7.util.DiffUtil
4 | import android.support.v7.widget.RecyclerView
5 | import android.view.LayoutInflater
6 | import android.view.ViewGroup
7 | import azadev.android.architecture.databinding.Part4ItemBinding
8 |
9 | typealias AdapterData = List
10 |
11 | class Part4ListAdapter(
12 | private var data: AdapterData = emptyList()
13 | ) : RecyclerView.Adapter() {
14 |
15 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Part4ListAdapter.ViewHolder {
16 | val binding = Part4ItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
17 | return Part4ListAdapter.ViewHolder(binding)
18 | }
19 |
20 | override fun onBindViewHolder(holder: Part4ListAdapter.ViewHolder, position: Int) {
21 | holder.binding.item = data[position]
22 | }
23 |
24 | override fun getItemCount() = data.size
25 |
26 | fun setData(newData: AdapterData) {
27 | val diffResult = DiffUtil.calculateDiff(DiffCallback(data, newData))
28 | data = newData
29 | diffResult.dispatchUpdatesTo(this)
30 | }
31 |
32 | class ViewHolder(val binding: Part4ItemBinding) : RecyclerView.ViewHolder(binding.root)
33 |
34 | class DiffCallback(
35 | private val oldData: AdapterData,
36 | private val newData: AdapterData
37 | ) : DiffUtil.Callback() {
38 |
39 | override fun getOldListSize() = oldData.size
40 |
41 | override fun getNewListSize() = newData.size
42 |
43 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
44 | oldData[oldItemPosition].id == newData[newItemPosition].id
45 |
46 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
47 | oldData[oldItemPosition] == newData[newItemPosition]
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/part5_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
18 |
19 |
25 |
26 |
33 |
34 |
41 |
42 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/app/src/main/java/azadev/android/architecture/feat/part5/Part5ListAdapter.kt:
--------------------------------------------------------------------------------
1 | package azadev.android.architecture.feat.part5
2 |
3 | import android.support.v7.util.DiffUtil
4 | import android.support.v7.widget.RecyclerView
5 | import android.view.LayoutInflater
6 | import android.view.ViewGroup
7 | import azadev.android.architecture.databinding.Part5ItemBinding
8 |
9 | typealias AdapterData = List
10 |
11 | class Part5ListAdapter(
12 | private val model: Part5ViewModel,
13 | private var data: AdapterData = emptyList()
14 | ) : RecyclerView.Adapter() {
15 |
16 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Part5ListAdapter.ViewHolder {
17 | val binding = Part5ItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
18 | return Part5ListAdapter.ViewHolder(binding)
19 | }
20 |
21 | override fun onBindViewHolder(holder: Part5ListAdapter.ViewHolder, position: Int) {
22 | holder.binding.model = model
23 | holder.binding.data = data[position]
24 | }
25 |
26 | override fun getItemCount() = data.size
27 |
28 | fun setData(newData: AdapterData) {
29 | val diffResult = DiffUtil.calculateDiff(DiffCallback(data, newData))
30 | data = newData
31 | diffResult.dispatchUpdatesTo(this)
32 | }
33 |
34 | class ViewHolder(val binding: Part5ItemBinding) : RecyclerView.ViewHolder(binding.root)
35 |
36 | class DiffCallback(
37 | private val oldData: AdapterData,
38 | private val newData: AdapterData
39 | ) : DiffUtil.Callback() {
40 |
41 | override fun getOldListSize() = oldData.size
42 |
43 | override fun getNewListSize() = newData.size
44 |
45 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
46 | oldData[oldItemPosition].id == newData[newItemPosition].id
47 |
48 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
49 | oldData[oldItemPosition] == newData[newItemPosition]
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/part3_login_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
20 |
21 |
28 |
29 |
38 |
39 |
46 |
47 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/app/src/main/java/azadev/android/architecture/feat/part2/Part2ListAdapter.kt:
--------------------------------------------------------------------------------
1 | package azadev.android.architecture.feat.part2
2 |
3 | import android.support.v7.util.DiffUtil
4 | import android.support.v7.widget.RecyclerView
5 | import android.view.LayoutInflater
6 | import android.view.ViewGroup
7 | import azadev.android.architecture.databinding.Part2ItemBinding
8 |
9 | typealias AdapterData = List
10 |
11 | class Part2ListAdapter(
12 | private val model: Part2ViewModel,
13 | private var data: AdapterData = emptyList()
14 | ) : RecyclerView.Adapter() {
15 |
16 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Part2ListAdapter.ViewHolder {
17 | val binding = Part2ItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
18 | return Part2ListAdapter.ViewHolder(binding)
19 | }
20 |
21 | override fun onBindViewHolder(holder: Part2ListAdapter.ViewHolder, position: Int) {
22 | val id = data[position]
23 |
24 | holder.binding.model = model
25 |
26 | holder.binding.id = id
27 | holder.binding.title.text = "Item $id"
28 | }
29 |
30 | override fun getItemCount() = data.size
31 |
32 | fun setData(newData: AdapterData) {
33 | val diffResult = DiffUtil.calculateDiff(DiffCallback(data, newData))
34 | data = newData
35 | diffResult.dispatchUpdatesTo(this)
36 | }
37 |
38 | class ViewHolder(val binding: Part2ItemBinding) : RecyclerView.ViewHolder(binding.root)
39 |
40 | class DiffCallback(
41 | private val oldData: AdapterData,
42 | private val newData: AdapterData
43 | ) : DiffUtil.Callback() {
44 |
45 | override fun getOldListSize() = oldData.size
46 |
47 | override fun getNewListSize() = newData.size
48 |
49 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
50 | oldData[oldItemPosition] == newData[newItemPosition]
51 |
52 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
53 | oldData[oldItemPosition] == newData[newItemPosition]
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/part5_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
10 |
11 |
14 |
15 |
16 |
17 |
22 |
23 |
27 |
28 |
36 |
37 |
46 |
47 |
55 |
56 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/app/src/main/java/azadev/android/architecture/feat/part3/Part3LoginDialogFragment.kt:
--------------------------------------------------------------------------------
1 | package azadev.android.architecture.feat.part3
2 |
3 | import android.app.Dialog
4 | import android.content.Context
5 | import android.os.Bundle
6 | import android.support.v4.app.DialogFragment
7 | import android.support.v7.app.AlertDialog
8 | import android.view.LayoutInflater
9 | import android.widget.Toast
10 | import azadev.android.architecture.R
11 | import azadev.android.architecture.core.arch.viewmodel.viewModel
12 | import azadev.android.architecture.databinding.Part3LoginDialogBinding
13 |
14 | class Part3LoginDialogFragment : DialogFragment() {
15 |
16 | private val model by viewModel()
17 |
18 | private lateinit var listener: Listener
19 |
20 | override fun onAttach(context: Context) {
21 | super.onAttach(context)
22 |
23 | listener = context as Listener
24 | }
25 |
26 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
27 | val dialog = setupDialog()
28 |
29 | model.validationErrorCommand.observe(this) {
30 | Toast.makeText(context, "Wrong credentials :(", Toast.LENGTH_SHORT).show()
31 | }
32 |
33 | model.loggedInCommand.observe(this) {
34 | listener.onLogin(model.username.value!!)
35 | }
36 |
37 | model.cancelledCommand.observe(this) {
38 | listener.onCancel()
39 | }
40 |
41 | return dialog
42 | }
43 |
44 | private fun setupDialog(): AlertDialog {
45 | val view = LayoutInflater.from(context).inflate(R.layout.part3_login_dialog, null, false)
46 |
47 | val binding = Part3LoginDialogBinding.bind(view)
48 | binding.setLifecycleOwner(this)
49 | binding.model = model
50 |
51 | val dialog = AlertDialog.Builder(activity!!)
52 | .setView(view)
53 | .setPositiveButton("Go", null)
54 | .setNegativeButton("Cancel", null)
55 | .create()
56 |
57 | isCancelable = false
58 |
59 | // Workaround to prevent buttons to close the dialog
60 | // https://stackoverflow.com/q/2620444/4899346
61 | dialog.setOnShowListener {
62 | dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
63 | model.handleLoginButtonClick()
64 | }
65 |
66 | dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
67 | model.handleCancel()
68 | }
69 | }
70 |
71 | return dialog
72 | }
73 |
74 | interface Listener {
75 | fun onLogin(username: String)
76 | fun onCancel()
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/part4_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
18 |
19 |
25 |
26 |
32 |
33 |
39 |
40 |
46 |
47 |
55 |
56 |
65 |
66 |
75 |
76 |
87 |
88 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/part1_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
20 |
21 |
25 |
26 |
32 |
33 |
39 |
40 |
43 |
44 |
51 |
52 |
61 |
62 |
72 |
73 |
74 |
75 |
83 |
84 |
89 |
90 |
94 |
95 |
98 |
99 |
104 |
105 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
171 |
172 |
173 |
174 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android Architecture with Kotlin – Best Practices
2 |
3 | **This sample project provides several examples of using modern architecture approaches.**
4 |
5 | For instance, it uses:
6 |
7 | * MVVM architectural pattern
8 | * Architecture Components:
9 | * LiveData
10 | * ViewModel
11 | * Room
12 | * Data Binding
13 |
14 | In addition, some examples use:
15 |
16 | * ConstraintLayout
17 | * RecyclerView
18 | * ViewHolder
19 | * DiffUtil
20 |
21 | 
22 |
23 | The project consists of several parts. I tried to make them as simple as possible,
24 | so each of them focuses on a particular functionality and nothing more.
25 |
26 | Therefore you can easily appeal to a certain part
27 | to figure out how to implement similar functionality in your own app.
28 |
29 | Unlike the official [Android Architecture Components samples][googlesamples-architecture],
30 | this project is intended to demonstrate how to implement certain cases you faces in the real life.
31 | It's more like tips, not complete apps. All these tips are implemented within a single app.
32 |
33 |
34 |
35 | ## Credits
36 |
37 | Before you get into the examples, I recommend to read/watch the following awesome tutorials.
38 | I sorted them in the order I think is the best for learning.
39 |
40 | So go ahead!
41 |
42 | ### Data Binding:
43 |
44 | * [Lisa Wray — Data Binding in a Kotlin world][TW9dSEgJIa8]
45 | * [Data Binding Library][data-binding]
46 |
47 | ### ViewModel & LiveData:
48 |
49 | * [Android Jetpack: ViewModel][5qlIPTDE274]
50 | * [Android Jetpack: LiveData][OMcDk2_4LSk]
51 | * [LiveData Clean Code using MVVM and Android Architecture Components][53468ed0dc1f]
52 | * [LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)][ac2622673150]
53 | * [Android Architecture: Communication between ViewModel and View][ce14805d72bf]
54 | * [ViewModels and LiveData: Patterns + AntiPatterns][21efaef74a54]
55 |
56 | ### Room:
57 |
58 | * [Android Jetpack: Room][SKWh4ckvFPM]
59 | * [Android Room with a View - Kotlin][room-with-a-view-kotlin]
60 | * [Android Persistence codelab][persistence]
61 |
62 | ### All together:
63 |
64 | * [Build an App with Architecture Components][build-app-with-arch-components]
65 | * [Android Architecture Components samples][googlesamples-architecture]
66 |
67 |
68 |
69 | ## Guide to App Architecture
70 |
71 | Considering the links above now we can imagine how the architecture should look like:
72 |
73 | 
74 |
75 | Here's a summary explanation of the different classes in the diagram, starting from the top:
76 |
77 | ### UI Controllers
78 |
79 | UI Controllers are activities or fragments in conjunction with corresponding XML-files if present.
80 | The only job of UI controllers is to know how to display data and pass on UI events,
81 | such as the user pressing a button.
82 |
83 | > **Note!**
84 | > UI Controllers neither contain the UI data, nor directly manipulate data.
85 | > Don't modify ViewModel's data directly from the UI, even if it looks much simpler, than implement a handler within ViewModel.
86 |
87 | ### ViewModels and LiveData
88 |
89 | These classes represent all of the data needed for the UI to display.
90 | ViewModels doesn't know anything about the UI. It just provides reactive fields that UI can observe and update itself.
91 | To perform ViewModel-to-UI communication use one of reactive patterns like [SingleLiveEvent or Event wrapper][ac2622673150].
92 |
93 | ### Repository
94 |
95 | This class is the single source of truth for all of our app's data and acts as a clean API for the UI to communicate with.
96 | ViewModels simply request data from the repository.
97 | They do not need to worry about whether the repository should load from the database or network,
98 | or how or when to persist the data. The repository manages all of this.
99 | As part of this responsibility, the repository is a mediator between different data sources.
100 |
101 | > **Note!**
102 | > This architecture stresses that each class in the diagram only stores a reference to the class or classes
103 | > directly "below it" and not any classes above it.
104 | > This means the ViewModel class will store a reference to the Repository class,
105 | > but not to the UI controller class above it or the Remote Data Source class two levels below it.
106 |
107 |
108 | ## Examples
109 |
110 | Now you're ready to explore the examples of this project.
111 |
112 | ### 1. Simple Form
113 |
114 | This part demonstrates how to create a regular user input form in a reactive way.
115 |
116 | It actively uses Data Binding, Two-Way Data Binding and binding expressions.
117 | The state of the form is perfectly persistent across configuration changes, thanks to ViewModel.
118 | It also utilizes Google's SingleLiveEvent to display a Toast after the form is submitted.
119 |
120 | 
121 |
122 | ### 2. Simple Reactive List
123 |
124 | Here we create a reactive and efficient list of items.
125 |
126 | It uses RecyclerView, Adapter and ViewHolder in conjustion with ViewModel and LiveData.
127 |
128 | 
129 |
130 | ### 3. Dialogs
131 |
132 | Example of a robust login dialog with a custom layout
133 | that perfectly preserves its state during configuration changes.
134 |
135 | It also performs a slight validation of input data.
136 |
137 | 
138 |
139 | ### 4. Room Database
140 |
141 | This part demonstrates the basic usage of the Room database framework.
142 |
143 | Both lists within the Activity are being updated immediately and reactively.
144 | Even the list that requests odd items only through the `WHERE` condition.
145 |
146 | Working with SQLite database has never been so pretty!
147 |
148 | 
149 |
150 | ### 5. Interactive List
151 |
152 | This example shows an approach to create a list with each item containing a CheckBox.
153 | CheckBox (like other Android components) isn't well suitable for the unidirectional data flow principle.
154 | It's actually not a big deal since we have the two-way data-binding feature,
155 | which is really convenient when the View is directly connected to a LiveData-field within the ViewModel.
156 | But things are getting quite confusing when we're building lists.
157 |
158 | To make the example more informative I've added a couple of features:
159 | - Total number of selected items.
160 | - Delay between clicking CheckBox and the actual updating of the dataset (simulating a request or something).
161 |
162 | 
163 |
164 |
165 |
166 | ## License
167 |
168 | This software is released under the MIT License.
169 | See [LICENSE.txt](LICENSE.txt) for details.
170 |
171 |
172 |
173 | [TW9dSEgJIa8]: https://www.youtube.com/watch?v=TW9dSEgJIa8
174 | [data-binding]: https://developer.android.com/topic/libraries/data-binding/
175 |
176 | [5qlIPTDE274]: https://www.youtube.com/watch?v=5qlIPTDE274
177 | [OMcDk2_4LSk]: https://www.youtube.com/watch?v=OMcDk2_4LSk
178 | [53468ed0dc1f]: https://android.jlelse.eu/lets-keep-activity-dumb-using-livedata-53468ed0dc1f
179 | [ac2622673150]: https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
180 | [ce14805d72bf]: https://android.jlelse.eu/android-architecture-communication-between-viewmodel-and-view-ce14805d72bf
181 | [21efaef74a54]: https://medium.com/androiddevelopers/viewmodels-and-livedata-patterns-antipatterns-21efaef74a54
182 |
183 | [SKWh4ckvFPM]: https://www.youtube.com/watch?v=SKWh4ckvFPM
184 | [room-with-a-view-kotlin]: https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/
185 | [persistence]: https://codelabs.developers.google.com/codelabs/android-persistence/
186 |
187 | [build-app-with-arch-components]: https://codelabs.developers.google.com/codelabs/build-app-with-arch-components/
188 | [googlesamples-architecture]: https://github.com/googlesamples/android-architecture-components
189 |
--------------------------------------------------------------------------------