Android Room+MVVM Part 3: ActivityResult API — Contract, RxJava To Dao, LiveData(ViewModel)
< == 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!