I'm practising with room and found the following use case which I don't know how to handle correctly:
In a social network, a user can get posts from various points of the application, for example, a user can get posts from the Home section of the application and the Search section.
Knowing this, I would like to know the best way to store these posts in my database so that, in subsequent entries to the same section, only load the publications from that section.
My first idea was to create a new column in my post entity which stores the section where the post was fetched. However, this idea does not convince me much since it is not scalable and maintainable.
Thanks for reading my question!
Perhaps consider this demo, pretty scalable (as will be shown)
First the Database stuff.
The table aka #Entity annotated class (also must be defined as one of the entities in the #Database annotation (aka the list of tables)).
#Entity
data class Post(
#PrimaryKey
val post_id: Long?=null,
val post_timestamp: Long= System.currentTimeMillis(),
val post_text: String,
val post_user: String,
val and_so_on: String,
val post_posted_view: String
)
Now the #Dao annotated interface (the functions that access the database):-
#Dao
interface PostDAO {
#Insert
fun insert(post: Post): Long
#Query("SELECT * FROM post WHERE post_posted_view=:post_posted_view")
fun getPostsForAView(post_posted_view: String): List<Post>
}
i.e. a fun to insert posts according to the "View"
Now the #Database annotated abstract class:-
#Database(entities = [Post::class], exportSchema = false, version = 1)
abstract class PostDatabase: RoomDatabase() {
abstract fun getPostDAO(): PostDAO
companion object {
private var instance: PostDatabase?=null
fun getInstance(context: Context): PostDatabase {
if (instance==null) {
instance=Room.databaseBuilder(context,PostDatabase::class.java,"postdatabase.db")
.allowMainThreadQueries() /* For brevity of demo */
.build()
}
return instance as PostDatabase
}
}
}
as a singleton so the one instance is retrieved from wherever being instantiated only the first time it is retrieved.
First a small-scale, just one view (Activity) that allows posts to be made and that also displays the posts (in an edit text for simplicity). So
MainActivty:-
class MainActivity : AppCompatActivity() {
lateinit var db: PostDatabase
lateinit var dao: PostDAO
lateinit var posts: TextView
lateinit var add_posts: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
posts = this.findViewById(R.id.posts)
add_posts = this.findViewById(R.id.add_posts)
add_posts.setOnClickListener {
addSomePosts(3)
getPostsForView()
}
db = PostDatabase.getInstance(this)
dao = db.getPostDAO()
getPostsForView()
}
fun getPostsForView(post_posted_view: String=this.javaClass.name) {
val sb = StringBuilder()
var afterFirstPost: Boolean=false
val retrieved_psosts = dao.getPostsForAView(post_posted_view)
for (post in retrieved_psosts) {
if (afterFirstPost) {
sb.append("\n")
} else {
sb.append("View Posts for View ${post_posted_view}. There are ${retrieved_psosts.size} posts.\n")
}
afterFirstPost = true
sb.append(post.post_timestamp).append("\t").append(post.post_text)
}
posts.setText(sb)
}
fun addSomePosts(numberToAdd: Int) {
for (i in 0..numberToAdd) {
dao.insert(Post(post_text = "${System.currentTimeMillis()}-i A Post",post_user = "The User", post_posted_view = this.javaClass.name, and_so_on = "blah"))
}
}
}
Small-scale Results
When the App is run then first nothing much :-
Click ADD POSTS a few times to mimic some posts (4 per click (even though 3 coded 0-3 = 4)) and then :-
If the App is run again then immediately (without clicking ADD POSTS) the same is displayed (i.e. the database has persisted and the posts remain).
Scaling Up
To scale up, another Activity is added along with Main Activity having a button added to invoke the OtherActivity. So MainActivity is now:-
class MainActivity : AppCompatActivity() {
lateinit var db: PostDatabase
lateinit var dao: PostDAO
lateinit var posts: TextView
lateinit var add_posts: Button
lateinit var other_activity: Button /*<<<<<<<<<< ADDED */
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
posts = this.findViewById(R.id.posts)
add_posts = this.findViewById(R.id.add_posts)
add_posts.setOnClickListener {
addSomePosts(3)
getPostsForView()
}
other_activity = this.findViewById(R.id.goto_other_activity) /*<<<<<<<<<< ADDED */
/*<<<<<<<<<< ADDED >>>>>>>>>>*/
other_activity.setOnClickListener {
intent = Intent(this,OtherActivity::class.java)
startActivity(intent)
}
db = PostDatabase.getInstance(this)
dao = db.getPostDAO()
getPostsForView()
}
fun getPostsForView(post_posted_view: String=this.javaClass.name) {
val sb = StringBuilder()
var afterFirstPost: Boolean=false
val retrieved_psosts = dao.getPostsForAView(post_posted_view)
for (post in retrieved_psosts) {
if (afterFirstPost) {
sb.append("\n")
} else {
sb.append("View Posts for View ${post_posted_view}. There are ${retrieved_psosts.size} posts.\n")
}
afterFirstPost = true
sb.append(post.post_timestamp).append("\t").append(post.post_text)
}
posts.setText(sb)
}
fun addSomePosts(numberToAdd: Int) {
for (i in 0..numberToAdd) {
dao.insert(Post(post_text = "${System.currentTimeMillis()}-i A Post",post_user = "The User", post_posted_view = this.javaClass.name, and_so_on = "blah"))
}
}
}
The OtherActivity code is, bar the layout and class name, identical to the original MainActivity code, it being:-
class OtherActivity : AppCompatActivity() {
lateinit var db: PostDatabase
lateinit var dao: PostDAO
lateinit var posts: TextView
lateinit var add_posts: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_other)
posts = this.findViewById(R.id.posts)
add_posts = this.findViewById(R.id.add_posts)
add_posts.setOnClickListener {
addSomePosts(3)
getPostsForView()
}
db = PostDatabase.getInstance(this)
dao = db.getPostDAO()
getPostsForView()
}
fun getPostsForView(post_posted_view: String=this.javaClass.name) {
val sb = StringBuilder()
var afterFirstPost: Boolean=false
val retrieved_psosts = dao.getPostsForAView(post_posted_view)
for (post in retrieved_psosts) {
if (afterFirstPost) {
sb.append("\n")
} else {
sb.append("View Posts for View ${post_posted_view}. There are ${retrieved_psosts.size} posts.\n")
}
afterFirstPost = true
sb.append(post.post_timestamp).append("\t").append(post.post_text)
}
posts.setText(sb)
}
fun addSomePosts(numberToAdd: Int) {
for (i in 0..numberToAdd) {
dao.insert(Post(post_text = "${System.currentTimeMillis()}-i A Post",post_user = "The User", post_posted_view = this.javaClass.name, and_so_on = "blah"))
}
}
}
Nothing else is changed, i.e. the database is as it was. NO changes at all anywhere in regards to names to be stored. But now if the App is run (restarted not uninstalled and run) then:-
Note that for creating the demo the App was actually uninstalled and so the 8 posts are different.
If the Other Activity button is clicked then:-
i.e. No posts displayed, as would initially be expected.
However if the ADD POSTS button is clicked a few times (4 to easily spot the difference in the number of posts per view) then:-
If the back button/icon is clicked, to return to, the MainActivity then:-
i.e. MainActivity's posts are displayed.
Click Other Activity and again the 16 posts.
The database itself, via App Inspection, should have 8 + 16 = 24 rows:-
So from what you have described it would be easily scalable. Even say if you had multiple users, then it would simply need the WHERE clause to include selecting the appropriate user.
Now say you wanted to present a list of the "Views" to allow custom selection of views. Then you could base this upon a query, as demonstrated using App Inspection, such as
Related
I've been reading some questions, answers and blogs about MVVM pattern in Android, and I've implemented it in my application.
My application consists of a MainActivity with 3 Tabs. Content of each tab is a fragment.
One of these fragments, is a List of Users stored on Room DB, which is where I've implemented the MVVM (implementing User object, ViewModel, Repository and Adapter with RecycleView).
In this same fragment, I have an "add User" button at the end that leads to a new activity where a formulary is presented to add a new user. In this activity I want to be sure that the full name of user not exists in my DB before saving it.
I was trying to use the same ViewModel to get full UserNames full name, but it seems that ViewModel is never initialized and I dont' know why.
I've read some questions about that viewmodel can't be used in different activities (I use it in MainActivity also in AddUser activity
This is my ViewModel:
class UserViewModel : ViewModel() {
val allUsersLiveData: LiveData<List<User>>
private val repository: UserRepository
init {
Timber.i("Initializing UserViewModel")
repository = UserRepository(UserTrackerApplication.database!!.databaseDao())
allUsersLiveData = repository.getAllUsers()
}
fun getAllUsersFullName(): List<String> {
return allUsersLiveData.value!!.map { it.fullname}
}
And my AddUser activity:
class AddUser : AppCompatActivity() {
private lateinit var userList:List<String>
private lateinit var binding: ActivityAddUserBinding
private val userViewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_user)
Timber.i("Add User OnCreate")
binding = ActivityAddUserBinding.inflate(layoutInflater)
setContentView(binding.root)
}
fun addUserClick(v : View){
//someCode
val userName = binding.constraintLayoutAddUser.etUserName!!.text.toString()
if(checkUserExistance(userName)) {
val text: String = String.format(
resources.getString(R.string.repeated_user_name),
userName
Snackbar.make(v, text, Snackbar.LENGTH_LONG).show()
{
else
{
lifecycleScope.launch {
UserTrackerApplication.database!!.databaseDao()
.insertUser(user)
Timber.i("User added!")
}
finish()
}
}
Debugging, I see the log "Initializing UserViewModel" when the fragment of MainActivity is started, but I can't see it when AddUser activity is called. So it seems it's not initializing correctly.
So the questions:
Is this a good approach? I'm making some design mistake?
Why the VM isn't initializing?
EDIT
I forgot to add this function. Calling userViewModel here is where I get the error:
private fun checkUserExistance(userName: String): Boolean {
var result = false
userList = userViewModel.getAllUsersNames()
for (usr in userList)
{
if(usr.uppercase() == userName.uppercase())
{
result = true
break
}
}
return result
}
EDIT 2
I added this on my "onCreate" function and started to work:
userViewModel.allUsersLiveData.observe(this, Observer<List<User>>{
it?.let {
// updates the list.
Timber.i("Updating User Names")
userList =userViewModel.getAllUsersNames()
}
})
if you take a look at by viewModels delegate you will see it's lazy it means it will initialize when it is first time accessed
#MainThread
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}
return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
}
when i try update model to room database. i expect it just update one time, but it update infinite number of times.
DAO
#Dao
interface UserDAO {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUserDAO(userModel : UserModel)
#Update
suspend fun updateUserDAO(userModel : UserModel)
#Query("select * from user_table where _id =:id")
fun readUserById(id : String) : LiveData<UserModel>
Repository
class UserRepository(private val userAPI: UserAPI, context : Context) {
private val userDAO : UserDAO
init {
val userDatabase: UserRoomDatabase = UserRoomDatabase.getInstance(context)
userDAO = userDatabase.getUserDao()
}
fun readUserByIdDAO(id : String): LiveData<UserModel> = userDAO.readUserById(id)
suspend fun insertUserDAO(userModel : UserModel) = userDAO.insertUserDAO(userModel)
suspend fun updateUserDAO(userModel : UserModel) = userDAO.updateUserDAO(userModel)
}
ViewModel
class UserViewModel(private val userRepository: UserRepository) : ViewModel() {
val userModelResponse: MutableLiveData<Resource<UserModel>> = MutableLiveData()
val listUserModelResponse: MutableLiveData<Resource<ArrayList<UserModel>>> = MutableLiveData()
fun readUserByIdDAO(id : String) : LiveData<UserModel> = userRepository.readUserByIdDAO(id)
fun insertUserDAO(userModel: UserModel) = viewModelScope.launch {
userRepository.insertUserDAO(userModel)
}
fun updateUserDAO(userModel: UserModel) = viewModelScope.launch {
userRepository.updateUserDAO(userModel)
}
}
then i use it in MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: UserViewModel
private lateinit var binding: ActivityMainBinding
private lateinit var auth : FirebaseAuth
private var userId = ""
override fun onStart() {
super.onStart()
val navController = findNavController(R.id.fragmentHome)
binding.bottomNavigationHome.setupWithNavController(navController)
val userAPI = UserAPI.getInstance()
val userRepository = UserRepository(userAPI,this)
viewModel =
ViewModelProvider(this, UserModelFactory(userRepository)).get(UserViewModel::class.java)
auth = Firebase.auth
userId = auth.currentUser!!.uid
viewModel.readUserByIdDAO(userId).observe(this,{ userFromDAO ->
userFromDAO.active = true
viewModel.updateUserDAO(userFromDAO)
Log.e(TAG,userFromDAO.toString())
})
}
override fun onStop() {
super.onStop()
Log.e(TAG,"Stop")
viewModel.readUserByIdDAO(userId).observe(this,{ userFromDAO ->
userFromDAO.active = false
viewModel.updateUserDAO(userFromDAO)
viewModel.updateUserAPI(userFromDAO)
})
}
what is my problem and how can i fix it?
Every day offers us many surprises; today I wish you all to have a spectacular day. I wish you all the best
The real question is, what do you want to do? If you update the database only at onstart and onstop why do you need livedata? You said in your comment you want to check user online or offline. Why did you write it to the database? Tell us what you want to achieve and we can find another solution..
BTW, this is my answer if you want to check user is online or not:
If you want to check user online or offline why don't you just check
is device online or offline? You can see how to do that here:
How to check if an android device is online
The above link tells you how to make a function to check your
connectivity status. If you want to do something automatically every
time the network state changes, you need to run that function to check
your connectivity every time while your app running. That is not
effective. You can use work manager if you want to automatically
detect network changes:
How to trigger work manager when wifi is connected in android?
So if you know when you want to check the network status, use the
first method. If you want to monitor network status and automatically
do something when status changes, use the second method.
Use an if statement so you won’t repeatedly write the same item back
if (!userFromDAO.active) {
userFromDAO.active = true
viewModel.updateUserDAO(userFromDAO)
}
Log.e(TAG,userFromDAO.toString())
I want to create a search function for my user to quick access to my items .
Well , the first thing is that i have my product in a room table(List) and store them in database and show them with a recyclerview in the my main activity(Home activity ) .
So i want code a Query to search between them after user click on button search .
I code my query and after use it in my home activity nothing happend .i'm using mvvm model. pls help me with this .
Code :
My Table (List of Product ) :
#Entity(tableName = "cart")
data class RoomTables(
#PrimaryKey(autoGenerate = true) val id: Int?,
#ColumnInfo val title: String,
#ColumnInfo val price: Int,
#ColumnInfo val image: Int,
#ColumnInfo var amount: Int
)
My dao :
#Query ("SELECT * FROM cart WHERE title LIKE :search")
fun searchItem (search : String?):List<RoomTables>
My Repository :
fun searchItem(search :String) = db.GetDao().searchItem(search)
My Viewmodel :
fun searchItem(search : String) = CoroutineScope(Dispatchers.Default).launch {
repository.searchItem(search)
}
And HomeActivity :
class HomeActivity : AppCompatActivity() {
lateinit var viewModelRoom: ViewModelRoom
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.home_activity)
val list = ArrayList<RoomTables>()
for (i in 0..20) {
list.add(
RoomTables(
null, "$i banana", 12,
R.drawable.bannana, 0
)
)
}
recycler_main.apply {
layoutManager = GridLayoutManager(this#HomeActivity, 2)
adapter = RecyclerAdapterMain(list, context)
}
val database = DataBaseRoom(this)
val repositoryCart = RepositoryCart (database)
val factoryRoom = FactoryRoom(repositoryCart)
viewModelRoom = ViewModelRoom(repositoryCart)
viewModelRoom = ViewModelProvider(this , factoryRoom ).get(ViewModelRoom::class.java)
val editText : EditText = findViewById(R.id.edittextSearch)
val searchbtn : ImageView = findViewById(R.id.search_main)
searchbtn.setOnClickListener{
viewModelRoom.searchItem(editText.text.toString())
}
Let's try this approach.
First get list items from the table.
#Query ("SELECT * FROM cart")
fun searchItem():List<RoomTables>
Now from your repository.
fun searchItem() : List<RoomTables> = db.GetDao().searchItem()
In ViewModel.
fun searchItem(search : String): <List<RoomTables> {
filterWithQuery(query)
return filteredList
}
private fun filterWithQuery(query: String, repository: YourRepository) {
val filterList = ArrayList<RoomTables>()
for (currentItem: RoomTables in repository.searchItem()) {
val formatTitle: String = currentItem.title.toLowerCase(Locale.getDefault())
if (formatTitle.contains(query)) {
filterList.add(currentItem)
}
}
filteredList.value = filterList
}
Make sure you add Coroutines above.
Now you have all elements filtered and returns new list items based on search query user entered.
In your fragment or activity observe data.
searchbtn.setOnClickListener{
viewModel.searchItem(query).observe(viewLifecycleOwner, Observer { items -> {
// Add data to your recyclerview
}
}
The flow and approach is correct and it is working, it is hard to follow your code since i'm not sure if the return types match because you are not using LiveData, in this case you must.
If you found confusing or hard to follow, i have a working example in github, compare and make changes.
https://github.com/RajashekarRaju/ProjectSubmission-GoogleDevelopers
I'm rewriting an app that involves retrieving data from a server via REST, saving that to the database on each Android device, and then displaying that data to the user. The data being retrieved from the server has a "since" parameter, so it won't return all data, just data that has changed since the last retrieval.
I have the retrieval from the server working fine, but I'm not sure the best way to save that data to the database, then show it to the user. I'm using Kotlin, Retrofit, Room and LiveData.
The code below is a simplified version of what I'm actually doing, but it gets the point across.
MyData.kt (model)
#Entity(tableName = "MyTable")
data class MyData(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id")
var id Int? = null,
#SerializedName("message")
#ColumnInfo(name = "message")
var message: String? = null
) {
companion object {
fun fromContentValues(values: ContentValues): MyData {
val data = MyData()
// Do this for id and message
if (values.containsKey("id") {
data.id = values.getAsInteger("id")
}
}
}
}
DataViewModel.kt
class DataViewModel(application: Application) : AndroidViewModel(application) {
private val repository = DataRepository()
fun data(since: Long) =
liveData(Dispatchers.IO) {
val data = repository.getDataFromServer(since)
emit(data)
}
fun saveData(data: List<MyData>) =
liveData(Dispatchers.Default) {
val result = repository.saveDataToDatabase(data)
emit(result)
}
fun data() =
liveData(Dispatchers.IO) {
val data = repository.getDataFromDatabase()
emit(data)
}
}
DataRepository.kt
class DataRepository(application: Application) {
// I won't add how the Retrofit client is created, it's standard
private var client = "MyUrlToGetDataFrom"
private var myDao: MyDao
init {
val myDatabase = MyDatabase.getDatabase(application)
myDao = myDatabase!!.myDao()
}
suspend fun getDataFromServer(since: Long): List<MyData> {
try {
return client.getData(since)
} catch (e: Exception) {
}
}
fun getDataFromDatabase(): List<MyData> = myDao.getAll()
suspend fun insertData(data: List<MyData>) =
myDao.insertData(data)
}
MyDao.kt
#Dao
interface PostsDao {
#Query("SELECT * FROM " + Post.TABLE_NAME + " ORDER BY " + Post.COLUMN_ID + " desc")
suspend fun getAllData(): List<MyData>
#Insert
suspend fun insertData(data: List<MyData>)
}
ListActivity.kt
private lateinit var mDataViewModel: DataViewModel
override fun onCreate(savedInstanceBundle: Bundle?) {
super.onCreate(savedInstanceBundle)
mDataViewModel = ViewModelProvider(this, DataViewModelFactory(contentResolver)).get(DataViewModel::class.java)
getData()
}
private fun getData() {
mDataViewModel.data(getSince()).observe(this, Observer {
saveData(it)
})
}
private fun saveData(data: List<MyData>) {
mDataViewModel.saveData(data)
mDataViewModel.data().observe(this, Observer {
setupRecyclerView(it)
})
}
ListActivity.kt, and possibly the ViewModel and Repository classes where it uses coroutines, are where I'm stuck. getData() retrieves the data from the server without a problem, but when it comes to saving it in the database, then taking that saved data from the database and displaying it to the user I'm unsure of the approach. As I mentioned I'm using Room, but Room will not let you access the database on the main thread.
Remember, I have to save in the database first, then retrieve from the database, so I don't want to call mDataViewModel.data().observe until after it saves to the database.
What is the proper approach to this? I've tried doing CoroutineScope on the mDataViewModel.saveData() then .invokeOnCompletion to do mDataViewModel.data().observe, but it doesn't save to the database. I'm guessing I'm doing my Coroutines incorrectly, but not sure where exactly.
It will also eventually need to delete and update records from the database.
Updated Answer
After reading comments and updated question I figured out that you want to fetch a small list of data and store it to database and show all the data stored in the database. If this is what you want, you can perform the following (omitted DataSouce for brevity) -
In PostDao You can return a LiveData<List<MyData>> instead of List<MyData> and observe that LiveData in the Activity to update the RecyclerView. Just make sure you remove the suspend keyword as room will take care of threading when it returns LiveData.
#Dao
interface PostsDao {
#Query("SELECT * FROM " + Post.TABLE_NAME + " ORDER BY " + Post.COLUMN_ID + " desc")
fun getAllData(): LiveData<List<MyData>>
#Insert
suspend fun insertData(data: List<MyData>)
}
In Repository make 2 functions one for fetching remote data and storing it to the database and the other just returns the LiveData returned by the room. You don't need to make a request to room when you insert the remote data, room will automatically update you as you are observing a LiveData from room.
class DataRepository(private val dao: PostsDao, private val dto: PostDto) {
fun getDataFromDatabase() = dao.getAllData()
suspend fun getDataFromServer(since: Long) = withContext(Dispatchers.IO) {
val data = dto.getRemoteData(since)
saveDataToDatabase(data)
}
private suspend fun saveDataToDatabase(data: List<MyData>) = dao.insertData(data)
}
Your ViewModel should look like,
class DataViewModel(private val repository : DataRepository) : ViewModel() {
val dataList = repository.getDataFromDatabase()
fun data(since: Long) = viewModelScope.launch {
repository.getDataFromServer(since)
}
}
In the Activity make sure you use ListAdapter
private lateinit var mDataViewModel: DataViewModel
private lateinit var mAdapter: ListAdapter
override fun onCreate(savedInstanceBundle: Bundle?) {
...
mDataViewModel.data(getSince())
mDataViewModel.dataList.observe(this, Observer(adapter::submitList))
}
Initial Answer
First of all, I would recommend you to look into Android Architecture Blueprints v2. According to Android Architecture Blueprints v2 following improvements can be made,
DataRepository should be injected rather than instantiating internally according to the Dependency Inversion principle.
You should decouple the functions in the ViewModel. Instead of returning the LiveData, the data() function can update an encapsulated LiveData. For example,
class DataViewModel(private val repository = DataRepository) : ViewModel() {
private val _dataList = MutableLiveData<List<MyData>>()
val dataList : LiveData<List<MyData>> = _dataList
fun data(since: Long) = viewModelScope.launch {
val list = repository.getData(since)
_dataList.value = list
}
...
}
Repository should be responsible for fetching data from remote data source and save it to local data source. You should have two data source i.e. RemoteDataSource and LocalDataSource that should be injected in the Repository. You can also have an abstract DataSource. Let's see how can you improve your repository,
interface DataSource {
suspend fun getData(since: Long) : List<MyData>
suspend fun saveData(list List<MyData>)
suspend fun delete()
}
class RemoteDataSource(dto: PostsDto) : DataSource { ... }
class LocalDataSource(dao: PostsDao) : DataSource { ... }
class DataRepository(private val remoteSource: DataSource, private val localSource: DataSource) {
suspend fun getData(since: Long) : List<MyData> = withContext(Dispatchers.IO) {
val data = remoteSource.getData(since)
localSource.delete()
localSource.save(data)
return#withContext localSource.getData(since)
}
...
}
In your Activity, you just need to observe the dataList: LiveData and submit it's value to ListAdapter.
private lateinit var mDataViewModel: DataViewModel
private lateinit var mAdapter: ListAdapter
override fun onCreate(savedInstanceBundle: Bundle?) {
...
mDataViewModel.data(since)
mDataViewModel.dataList.observe(this, Observer(adapter::submitList))
}
Activity receiving intent
class AddNoteActivity : AppCompatActivity() {
private lateinit var addViewModel: NoteViewModel
private lateinit var titleEditText: TextInputEditText
private lateinit var contentEditText: TextInputEditText
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_note_activty)
setSupportActionBar(toolbar)
addViewModel = ViewModelProviders.of(this).get(NoteViewModel::class.java)
titleEditText = findViewById(R.id.itemTitle)
contentEditText = findViewById(R.id.itemNote)
val extra = intent.extras
if (extra != null) {
val uuid = extra.getLong("UUID")
val note: Notes? = addViewModel.getNote(uuid)
titleEditText.setText(note!!.title)
contentEditText.setText(note.note)
}
}
}
NoteViewModel class
class NoteViewModel(application: Application) : AndroidViewModel(application) {
companion object {
private var note: Notes = Notes(0, "", "test title", "test ontent")
}
fun getNote(uuid: Long?): Notes {
val job = async(CommonPool) {
getNoteAsyncTask(notesDatabase).execute(uuid)
}
runBlocking { job.await() }
return note
}
class getNoteAsyncTask(database: NotesDatabase) : AsyncTask<Long, Unit, Unit>() {
private val db: NotesDatabase = database
override fun doInBackground(vararg params: Long?) {
note = db.notesDataDao().getNote(params[0])
}
}
}
If I pass an intent to get a Note object from the database with a uuid and set that received data in titleEditText and contentEditText, the data set in the Note was from previous intent invoked when we clicked on the Note item in RecyclerView. On clicking the Note item for the first time, I get the default value which I have set "test title" and "test content".
Aforementioned is the behavior most of the time. Sometimes the data set in titleEditText and contentEditText is of the correct Note object.
Can someone please tell me what I have done wrong? How can I correct my apps behavior?
Unfortunately, there is a big mistake in how you use a view model to provide a data to your view(AddNoteActivity).
Basically, your view never has a chance to wait for the data to be fetched as it always receives a default value. This happens because the AsyncTask runs on its own thread pool so the coroutine completes immediately and returns a default value.
You should consider using LiveData to post a new object to your view and refactor your view model.
So, you need to make a query to the database synchronous and observe changes to a note rather than have a getter for it. Of course, in a real life scenario it might be a good idea to have different kind of states to be able to show a spinner while a user is waiting. But this is another big question. So to keep things simple consider changing your view model to something like that:
class NoteViewModel(private val database: NotesDatabase) : ViewModel { // you do not need an application class here
private val _notes = MutableLiveData<Notes>()
val notes: LiveData<Notes> = _notes
fun loadNotes(uuid: Long) {
launch(CommonPool) {
val notes = database.notesDataDao().getNote(uuid)
_notes.setValue(notes)
}
}
}
Then, you can observe changes to the note field in your activity.
class AddNoteActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
val noteViewModel = ViewModelProviders.of(this).get(NoteViewModel::class.java)
noteViewModel.notes.observe(this, Observer {
title.text = it.title
content.text = it.note
})
}
}
Also you need to use a ViewModelProvider.Factory to create your view model and properly inject dependencies into it. Try to avoid having a context there as it makes it much harder to test.