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.
Related
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
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)
}
I have a property with late initialization.
Now I want to provide a live data which does not emit anything until the property is initialized completely.
How to do this in proper Kotlin way?
class SomeConnection {
val data: Flow<SomeData>
...
class MyViewModel {
private lateinit var _connection: SomeConnection
// private val _connection: CompletableDeferred<SomeConnection>()
val data = _coonection.ensureInitilized().data.toLiveData()
fun connect(){
viewModelScope.launch {
val conn = establishConnection()
// Here I have to do something for the call ensureInitilized to proceed
}
}
private suspend fun establishConnection(){
...
}
Declare a MutableLiveData emitting values of type SomeConnection and a corresponding LiveData.
private val _connectionLiveData = MutableLiveData<SomeConnection>()
val connectionLiveData: LiveData<SomeConnection> = _connectionLiveData
Then assign value to _connectionLiveData when _connection is initialized:
if (::_connection.isInitialized()) _connectionLiveData.value = _connection
(or _connectionLiveData.postValue(_connection) if your code works concurrently)
Now observe this LiveData in another place in code, I'll use fragment here for the sake of example:
override fun firstOnViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.connectionLiveData.observe(this, ::sendData)
}
Then send the desired data via the corresponding view model method
I'm fairly new to Kotlin/Android development, and am trying to figure out the best way to update data in a Room database. After following some tutorials, I currently have an architecture that looks like this:
Room Database with tables and DAOs -> Repository -> ViewModel -> Activity
So the activity has a ViewModel that calls the Repository, which in turn updates the database.
The ViewModel for the activity has a LiveData list of the object (there's also a factory to create the ViewModel, but that's just to allow the bookId to be passed in):
class ViewBookViewModel(application: Application, bookId: Int) : AndroidViewModel(application) {
private val repository: AppRepository
internal val flashCards: LiveData<List<FlashCard>>
init {
val flashCardDao = AppDatabase.getDatabase(application, viewModelScope).flashCardDao()
val bookDao = AppDatabase.getDatabase(application, viewModelScope).bookDao()
repository = AppRepository(flashCardDao, bookDao)
flashCards = flashCardDao.getByBookId(bookId)
}
fun insert(flashCard: FlashCard) = viewModelScope.launch(Dispatchers.IO){
repository.insert(flashCard)
}
fun setIsFavorited(cardUid: Long, favorited: Boolean) = viewModelScope.launch(Dispatchers.IO) {
repository.setIsFavorited(cardUid, favorited)
}
}
//The actual query that gets called eventually
#Query("UPDATE flashcard SET is_favorited = :favorited WHERE uid LIKE :cardUid")
fun setFavorited(cardUid: Long, favorited: Boolean)
And the Activity sets up the viewModel and also creates an observer on the
class ViewBookActivity : AppCompatActivity() {
private lateinit var flashCards: LiveData<List<FlashCard>>
private var layoutManager: RecyclerView.LayoutManager? = null
private lateinit var viewModel: ViewBookViewModel
private var bookId: Int = 0
private lateinit var bookTitle: String
override fun onCreate(savedInstanceState: Bundle?) {
...
bookId = intent.extras["bookId"] as Int
bookTitle = intent.extras["bookTitle"].toString()
layoutManager = LinearLayoutManager(this)
flashCardRecyclerView.layoutManager = layoutManager
viewModel = ViewModelProviders.of(this, ViewBookViewModelFactory(application, bookId as Int)).get(ViewBookViewModel::class.java)
flashCards = viewModel.flashCards
flashCards.observe(this, Observer { flashCards:List<FlashCard> ->
flashCardRecyclerView.adapter = FlashCardRecyclerAdapter(flashCards, viewModel)
})
}
}
Finally, I have a custom RecyclerAdapter, which is where I'm running into trouble. I have it set up so that when the user taps the "favorite" button on the Flash Card, it updates the database. However, this also causes the Activity to "refresh", scrolling to the top. I assume this is because it is observing LiveData, and that data is being changed.
custom RecylcerAdapter with ViewHolder code (stripped not-relevant code):
class FlashCardRecyclerAdapter(val flashCards: List<FlashCard>, val viewModel: ViewBookViewModel) : RecyclerView.Adapter<FlashCardRecyclerAdapter.FlashCardViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FlashCardViewHolder {
val v: View = LayoutInflater
.from(parent.context)
.inflate(R.layout.flash_card, parent, false)
return FlashCardViewHolder(v)
}
override fun onBindViewHolder(holder: FlashCardViewHolder, position: Int) {
val card = flashCards[position]
holder.isFavorited = card.isFavorited
holder.uid = card.uid
holder.modifyFavoriteButtonImage(holder.isFavorited)
}
override fun getItemCount(): Int {
return flashCards.size
}
inner class FlashCardViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
var mFavorited: Button
var frontShowing: Boolean
var isFavorited: Boolean = false
var uid: Long = 0
init {
mFavorited = itemView.findViewById(R.id.favoriteButton)
mFavorited.setOnClickListener { _ ->
isFavorited = !isFavorited
viewModel.setIsFavorited(uid, isFavorited) // Here is the database call
modifyFavoriteButtonImage(isFavorited)
}
}
fun modifyFavoriteButtonImage(isFavorited: Boolean){
// Code removed, just updates the image to be a filled/empty star based on favorited status
}
}
I feel like I am probably doing something wrong, as passing the ViewModel into the recylcer adapter in order to update the DB does not seem correct. Is there a pattern I should be using for this sort of situation, or should I change the code to not be using LiveData? Any help would be greatly appreciated.
flashCards.observe(this, Observer { flashCards:List<FlashCard> ->
flashCardRecyclerView.adapter = FlashCardRecyclerAdapter(flashCards, viewModel)
}
you should not be making a new adapter instance here, instead, assign the values you get from the live data to the existing adapter (adapter.flashCards = flashCards, LiveData value) and call adapter.notifyDataSetChanged, this will tell your adapter that new data came in and it needs to update.
you should not be passing your ViewModel to your adapter (or anything).
you can do something like this instead:
class FlashCardRecyclerAdapter(val flashCards: List<FlashCard>, val callback:(FlashCard) -> Unit)
then, where you declare your adapter, you do this :
val adapter = FlashCardRecyclerAdapter(...) {
viewModel.update(it)
}
and then :
override fun onBindViewHolder(holder: FlashCardViewHolder, position: Int) {
val card = flashCards[position]
holder.isFavorited = card.isFavorited
holder.uid = card.uid
holder.itemView.setOnClickListener {
callback.invoke(card)
}
holder.modifyFavoriteButtonImage(holder.isFavorited)
}
In your repository method, I am not sure what you are doing there but rather than passing in a livedata instance, you should pass in the underlying data of the livedata instance. That way, the observer in the main activity doesn't get triggered everytime you call setIsFavorited(). If you do want to trigger the observer, then you can just call postValue() on the livedata instance. As for the adapter question, I do not know the best practices but I usually create a listener interface so I don't have to pass around my viewmodels everywhere. All of my viewmodels are contained within my fragments and never goes anywhere else. Let me know if this answers your questions.
Also, if you are using viewmodels with recyclerview, consider using list adapters. They are made to work seamlessly with viewmodels. https://developer.android.com/reference/android/support/v7/recyclerview/extensions/ListAdapter
It makes it much simpler to use viewmodels with recyclerview.
I want to move from one Activity which displays a RecyclerView to another Activity (detail). But when I added data transmission via Intent, the data always failed to be taken in the Activity detail.
This is the error:
My MainDetail:
private lateinit var viewModel: MainDetailModel
var idAnime: String = "34134"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_detail)
idAnime = intent.getStringExtra("idAnime")
println("idanime $idAnime")
setupFragment()
setupViewModel()
}
}
ViewModel:
class MainViewModel(context: Application, private val appRepository: AppRepository, private val contexts: Context) : AndroidViewModel(context), MainItemClickAction {
override fun onItemClicked(detailModel: DetailModel) {
var intent = Intent(contexts, MainDetailActivity::class.java)
intent.putExtra("idAnime",detailModel.mal_id )
contexts.startActivity(intent)
}
}
Check if your field "detailModel.mal_id" mal_id in that case is string, because you're requesting string in details activity. If it's string check also if this "mal_id" is null. Other issues from code you provided can't be seen.
Check your value idAnime, it exists or not, I think is better check all value is empty or not before putting into listView or another view.