Android Room+MVVM Part 3: ActivityResult API — Contract, RxJava To Dao, LiveData(ViewModel)

Homan Huang
7 min readAug 11, 2020

--

< == Part Two

In this part, I will implement the code with RxJava with Room database, build an explicit activity contract, parallel working with LiveData. Let’s begin.

— ===Menu=== —

🔌1. Gradle: Add RxJava
💢2. RxJava to Dao
👆 3. RxJava: Insert One User
🏄4. MainActivity: ActivityResult API — Contract
📲 5. NewUserActivity: Validate and Save Data
🚴6. Run

🔌1. Gradle: Add RxJava

==>Menu

In August 2020, Room doesn’t support RxJava3 yet. Let’s continue to change RxJava to 2.2.19.

// rxjava2
def rxJavaV = '2.2.19'
def rxAndroidV = '2.1.1'
// RxJava
implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidV"
implementation "io.reactivex.rxjava2:rxjava:$rxJavaV"

💢2. RxJava2 to Dao

==>Menu

Zeng…This is Dao! The LiveData is good with the View. Let’s keep they in getAll() or getOne() to read the Database. Those write functions, we can use RxJava, such as @Insert(1 or x), @Update( 1 ), and @Delete(1 or x). So we can withdraw the works from the MainThread.

From Android Developer,

In RxJava, you can pick Single, Maybe or Completable; so I choose Completable.

@Dao
interface UserDao {
...

// insert group of users
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(users: List<UserTable>?) : Completable

...
}

At UserDatabase.kt,

private fun insertData(
database: UserDatabase?,
users: List<UserTable>) {

database!!.runInTransaction {
val rxData = database.userDao()!!.insertAll(users)
rxData.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
// onComplete
{ lgd("RxJava: Insert Success") },
// onError
{ lgd("RxJava: Insert Error") }
).let {
CompositeDisposable().dispose()
}
}
}

I add variable rxData to take care of the async return. Schedulers.io manages data in the background. Observer monitors status at the MainThread. Completable has two types of return action: Complete and Error.

Let’s delete the database at Device File Explorer.

Commit the work and run. The Logcat shows:

D/MYLOG UserDatabase: getInstance()
D/MYLOG UserDatabase: userDb: com...UserDatabase_Impl@396cea6
D/MYLOG UserDatabase: Insert Demo Data
D/MYLOG UserDatabase: RxJava: Insert Success
D/MYLOG MainActivity: user size: 5

The demo data has displayed on the emulator. Usually, UserViewModel doesn’t insert a group of users except for the system administrator.

👆3. RxJava: Insert One User

==>Menu

In past parts, we have seen the app process in these pathes:

User List: MainActivity=> UserViewModel=> UserRespository=> UserDatabase=> UserDao(getAllByLiveData)=> RecyclerAdapter

User Detail: RecyclerAdapter(user)=> MainActivity(user)=> UserDetailViewModel(user)=> UserDetailFragment

All of them are about reading the database.

Let’s design a write path.

Add User: MainActivity(launch newUserContract)=> NewUserActivity(observe insert)=> NewUserModel(insert)=> UserManager(user)=> UserRepository(user)=> UserDatabase(user)=> User(insert(user):Single<Long>)=> NewUserActivity(observe insert)=> MainActivity(newUserContract)

It’s a loop. The NewUserActivity is observing for the new user input and return the result back to MainActivity. Let’s start to work.

DAO: Rx Return Single<Long>

At UserDao.kt, with Single of RxJava.

// insert one user
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(user: UserTable): Single<Long>

Activity w/. ViewModel: NewUser

UI: NewUserActivity is gathering user input.

Now we can code NewUserActivity.kt with auto injected NewUserViewModel by Hilt.

@AndroidEntryPoint
class NewUserActivity : AppCompatActivity() {

// viewModel
private val newUserViewModel: NewUserViewModel by viewModels()

NewUserViewModel.kt with injected UserManager by Hilt.

class NewUserViewModel @ViewModelInject constructor(
private val userManager: UserManager
) : ViewModel() {

companion object {
// logcat
val tag = "MYLOG "+"NewUserVM"
fun lgi(s: String) = Log.i(tag, s)
fun lgd(s: String) = Log.d(tag, s)
fun lge(s: String) = Log.e(tag, s)
}

}

The NewUserViewModel provides observable, isUserInserted, to monitor the insert process.

// LiveData: check user inserted
private val isUserInserted = MutableLiveData<Boolean>()
fun getStatus(): LiveData<Boolean> = isUserInserted
fun setValue() { isUserInserted.value = true }

And it provides insert new user ability to act as a presenter.

fun insertUser(name: String, address: String) {
isUserInserted.value = false // initial value
lgd("Insert new user")
userManager.registerUser(name, address, this)
}

UserManager.kt, with injected UserRepository by Hilt. It provides registerUser() to call UserRepository.

@Singleton
class UserManager @Inject constructor(
private val userRepository: UserRepository
) {
fun registerUser(
name: String,
address: String,
newUserViewModel: NewUserViewModel
) {
val user = UserTable()
user.name = name
user.address = address

userRepository.insertNewUserRx(user, newUserViewModel)
}
}

UserRepository.kt, it calls Database=>UserDao to do the insert. If the Single has no error, set the value of the observable of NewUserViewModel. Don’t be confused at this part. There are two teams of observer:

  • Rx(watch background database)
  • ViewModel(finish the activity and return to Main).
@Singleton
class UserRepository @Inject constructor() {
...

fun insertNewUserRx(
user: UserTable,
newUserViewModel: NewUserViewModel
) {
lgd("insertNewUserRx()")
val rxSingle = database.userDao()!!.insert(user)
rxSingle.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : SingleObserver<Long> {
override fun onSuccess(id: Long) {
lgd("User ID: $id")
newUserViewModel.setValue()
}

override fun onSubscribe(d: Disposable) {}

override fun onError(e: Throwable) {
lge("error: ${e.message}")
}
}).let {
CompositeDisposable().dispose()
}
}
...
}

The newUserViewModel.setValue() validates the insert process.

At NewUserActivity.kt, we need to provide the second observer team by LiveData.

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.add_user)

...

// oberseve insert user process
newUserViewModel.getStatus().observe(
this,
Observer<Boolean> { inserted ->
if (inserted) {
returnToMain()
}
}
)
...
}
...
//back to the main
fun returnToMain() {
val data = user_name_et.text.toString()
lgd("Ready to send data: $data")
val intent = Intent(this, MainActivity::class.java)
intent.putExtra(USERNAME, data)
setResult(Activity.RESULT_OK, intent)
finish()
}

🏄4. MainActivity: ActivityResult API — Contract

==>Menu

1. Gradle: Andriodx.Activity and Androidx.Fragment

def activity_version = "1.2.0-alpha07"
def fragment_version = "1.3.0-alpha07"
// Kotlin
implementation "androidx.activity:activity-ktx:$activity_version"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
// Testing Fragments in Isolation
debugImplementation "androidx.fragment:fragment-testing:$fragment_version"

Sync.

2. ActivityResult API: Create Contract

Let’s create an Explicit Contract in the main package.

class Main_NewUse_ActivityContract: 
ActivityResultContract<Int, String?>() {

override fun createIntent(
context: Context,
input: Int?): Intent {
return Intent(context, NewUserActivity::class.java).apply {
putExtra(NewUserActivity.ID, input)
}
}

override fun parseResult(resultCode: Int, intent: Intent?):
String? = when {
resultCode != Activity.RESULT_OK -> null
else -> intent?.getStringExtra(NewUserActivity.USERNAME)
}
}

ActivityResultContract<Input, Output>:

  • Input == Request Key in the first activity
  • Output == the return Object from second activity

At NewUserActivity.kt: It provides bundle keys.

companion object {
val USERNAME = "UNKNOWN NAME"
val ID = "NEW USER ID"
...

3. ActivityResult API: Register Contract

At MainActivity.kt,

class MainActivity : FragmentActivity(), UserClickEvent {

...

// Contract Registration
private val newUserContract = registerForActivityResult(
Main_NewUse_ActivityContract()
) { newUserName: String? ->
lgd("Returned Data: $newUserName")

// Handle the returned result
if (newUserName != null)
msg(this, "Welcome New User: $newUserName")
else
msg(this, "Add New User Cancelled!")
}

4. ActivityResult API: Call Contract

At MainActivity.kt,

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

lgi("MainActivity: onCreate()")

...

// add new user
val addUserFab = findViewById<FloatingActionButton>(R.id.addUser_fab)
addUserFab.setOnClickListener {
// call contract
newUserContract.launch(newUserActivityRequestCode)

}

I launch the contract at the addUserFab button.

📲5. NewUserActivity: Validate and Save Data

==>Menu

At NewUserActivity.kt, we need to configure the task of a button, keyboard setting, and validate the data.

SoftKeyboard Setting

In the world of EditText, you always need to set the keyboard’s visibility. Let’s detect the keyboard ON/OFF by viewTreeObserver.

private var keyboardVisible = false

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.add_user)

// check keyboard visibility
rootview.viewTreeObserver.addOnGlobalLayoutListener {
val r = Rect()
rootview.getWindowVisibleDisplayFrame(r)
val screenHeight: Int = rootview.rootView.height
val keypadHeight = screenHeight - r.bottom
keyboardVisible = keypadHeight > screenHeight * 0.15
}
...

Show SoftKeyboard:

private fun showKeyboard() {
if (!keyboardVisible) {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0)
}
}

Hide SoftKeyboard:

private fun hideKeyboard(view: View) {
if (keyboardVisible) {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view.windowToken, 0)
}
}

Validate User Input

fun validateInput(view: View): Boolean {
val name = user_name_et.text
val address = user_address_et.text

val chkName = TextUtils.isEmpty(name.trimStart().trimEnd())
val chkAddr = TextUtils.isEmpty(address.trimStart().trimEnd())

if (chkName || chkAddr) {
if (chkName) {
val builder = AlertDialog.Builder(this)
builder
.setTitle("Empty Warning...")
.setMessage("Your Name is Empty!")
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton("Input, please!") { _, _ ->
user_name_et.text = null
user_name_et.requestFocus()
showKeyboard()
}
.show()
}
if (chkAddr) {
val builder = AlertDialog.Builder(this)
builder
.setTitle("Empty Warning...")
.setMessage("Your Address is Empty!")
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton("Input, please!") { _, _ ->
user_address_et.text = null
user_address_et.requestFocus()
showKeyboard()
}
.show()
}
return false
} else {
return true
}
}

Set Buttons

The save_fab button:

save_fab.setOnClickListener {
if (validateInput(it)) {
hideKeyboard(it)
newUserViewModel.insertUser(
user_name_et.text.toString(),
user_address_et.text.toString())
}
}

The back_fab3 button:

back_fab3.setOnClickListener {
this.onBackPressed()
}

Set Back button: It provides a warning if the input is not empty.

override fun onBackPressed() {
val name = user_name_et.text
val address = user_address_et.text
if (!TextUtils.isEmpty(name.trimStart().trimEnd()) ||
!TextUtils.isEmpty(address.trimStart().trimEnd())) {
val builder = AlertDialog.Builder(this)
builder
.setTitle("Exit Warning...")
.setMessage("Your Data is Not Empty!\n\nExit?")
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton( "Yes" ) { _, _ ->
hideKeyboard(window.decorView.rootView)
super.onBackPressed()
}
.setNegativeButton("No") { _, _ ->
msg(this, "Continue...")
user_name_et.requestFocus()
showKeyboard()
}
.show()
} else {
lgd("Exit .")
hideKeyboard(window.decorView.rootView)
super.onBackPressed()
}
}

🚴6. Run

==>Menu

Finally, let run to test this simple task, add a new user. Commit the work first; then you can start the test journal: debug, test, fix the bug.

So far so good, please enjoy this reading.

Hope you learn something new today!

--

--

Homan Huang
Homan Huang

Written by Homan Huang

Computer Science BS from SFSU. I studied and worked on Android system since 2017. If you are interesting in my past works, please go to my LinkedIn.

No responses yet