im really new on AAC and repository.
I have made an app with MVVM and repository.
Activity
class UserTestActivity : AppCompatActivity() {
private val userViewModel : UserViewModel by viewModel<UserViewModel>()
private lateinit var button : AppCompatButton
private var count : Int =0
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user_test
button = findViewById(R.id.testButton)
val userObserver = Observer<MutableList<UserModel>> { newUserList ->
Log.d("room-db-status", "size: "+newUserList.size)
}
userViewModel._user.observe(this, userObserver)
button.setOnClickListener(View.OnClickListener {
count++
Toast.makeText(this, "updated: "+count, Toast.LENGTH_SHORT).show()
userViewModel.insertUser(UserModel(
uid = count.toString(),
nickName = "Alexar",
gender ="female",
age = 22,
birth ="19990901",
mainResidence= "Seoul",
subResidence = "???",
tripWish = mutableListOf("!!!","!!?"),
tripStyle = mutableListOf("!!!","!!?"),
selfIntroduction = "hi -_-",
uriList = mutableListOf("!!!","!!?"),
geohash = "none",
latitude = 37.455,
longitude = 124.890,
mannerScore = 4.5,
premiumOrNot = false,
knock = 0
))
})
}
}
this is ViewModel
class UserViewModel (
private val userRepository : UserRepository): ViewModel() {
val _user : LiveData<MutableList<UserModel>> = liveData(Dispatchers.IO) {
val data = userRepository.getAllUser()
emit(data)
}
fun insertUser (userModel: UserModel) {
viewModelScope.launch(Dispatchers.IO) {
userRepository.insertUser(userModel)
}
}
}
Repositoty
interface UserRepository {
suspend fun getAllUser () : MutableList<UserModel>
suspend fun insertUser (userModel: UserModel)
}
RepositoryImpl
class UserRepositoryImpl (
private val localDataSource : UserLocalDataSource,
private val userMapper: UserMapper) :UserRepository{
override suspend fun getAllUser() : MutableList<UserModel> {
val userList : List<UserEntity> = localDataSource.getAllUser()
var temp = mutableListOf<UserModel>()
for (user in userList)
temp.add(userMapper.entityToModel(user))
return temp
}
override suspend fun insertUser(userModel: UserModel) {
return localDataSource.insertUser(userMapper.modelToEntity(userModel))
}
}
UserLocalDataSource
interface UserLocalDataSource {
suspend fun getAllUser () : MutableList<UserEntity>
suspend fun insertUser (userEntity: UserEntity)
}
UserLocalDataSourceImpl
class UserLocalDataSourceImpl(
private val appDatabase: AppDatabase) : UserLocalDataSource {
override suspend fun getAllUser() : MutableList<UserEntity> {
return appDatabase.userEntityDao().getAllUser()
}
override suspend fun insertUser(userEntity: UserEntity) {
appDatabase.userEntityDao().insertUser(userEntity)
}
}
UserEntityDao
interface UserEntityDAO {
#Query ("SELECT * FROM user " )
suspend fun getAllUser() : MutableList<UserEntity>
#Query ("SELECT * FROM user WHERE uid = (:uid) ")
suspend fun getUser(uid: String) :UserEntity
#Insert (onConflict = REPLACE)
suspend fun insertUser (user : UserEntity)
#Query("DELETE FROM user WHERE uid = (:uid)")
suspend fun delete(uid : String)
}
there are also Mapper and Koin injection.
when trying to insert user data to room, it was successful. but
after that, liveData Builder
val _user : LiveData<MutableList<UserModel>> = liveData(Dispatchers.IO) {
val data = userRepository.getAllUser()
emit(data)
}
not invoked...
Of course, that builder is invoked only once when app started haha
who knows why??
I do not know.
You shouldn't rely on the liveData builder to be invoked when data is inserted/updated in DB. Please refer to this doc on how to work with liveData builder.
To achieve the case when an observer of LiveData is invoked when data is inserted/updated in DB, methods must return LiveData object in UserEntityDao:
interface UserEntityDAO {
#Query ("SELECT * FROM user " )
suspend fun getAllUser() : LiveData<List<UserEntity>>
#Query ("SELECT * FROM user WHERE uid = (:uid) ")
suspend fun getUser(uid: String) : LiveData<UserEntity>
// ...
}
Related
I am not sure what is exactly happening but when ApiService.apiService.getPokemon(name) in fun getPokemon in PokemonRepository.kt is called then the function getPokemon stops executing and emited livedata are then observed as null in DetailAktivity.kt instead of a valid Pokemon class.
I have checked the API call and it is working in other cases. I am new to Android programming, so I would appreciate some detailed explanation.
Here are the classes:
PokemonRepository.kt
class PokemonRepository(context: Context) {
companion object {
private val TAG = PokemonRepository::class.java.simpleName
}
private val pekemonDao = PokemonDatabase.getInstance(context).pokemonDao()
fun getPokemon(name: String) = liveData {
val disposable = emitSource(
pekemonDao.getOne(name).map {
it
}
)
val pokemon = ApiService.apiService.getPokemon(name)
try {
disposable.dispose()
pekemonDao.insertAllPokemons(pokemon)
pekemonDao.getOne(name).map {
it
}
} catch (e: Exception) {
Log.e(TAG, "Getting data from the Internet failed", e)
pekemonDao.getOne(name).map {
e
}
}
}
DetailActivity.kt
class DetailActivity : AppCompatActivity() {
companion object {
const val ITEM = "item"
}
private lateinit var binding: ActivityDetailBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
val vm: DetailViewModel by viewModels()
vm.pokemon.observe(
this,
{
binding.name.text = it.name
supportActionBar?.apply {
setDisplayShowTitleEnabled(true)
title = it.name
}
}
)
intent.extras?.apply {
vm.setCharacterId(getString(ITEM)!!)
}
}
}
DetailViewModel
class DetailViewModel(application: Application) : AndroidViewModel(application) {
private val repository = PokemonRepository(application)
private val name: MutableLiveData<String> = MutableLiveData()
val pokemon = name.switchMap { name ->
repository.getPokemon(name)
}
fun setCharacterId(characterId: String) {
name.value = characterId
}
}
ApiService.kt
interface ApiService {
#GET("pokemon?offset=0&limit=151")
suspend fun getPokemons(#Query("page") page: Int): NamedApiResourceList
#GET("pokemon/{name}")
suspend fun getPokemon(#Path("name") name: String): Pokemon
companion object {
private const val API_ENDPOINT = "https://pokeapi.co/api/v2/"
val apiService by lazy { create() }
private fun create(): ApiService = Retrofit.Builder()
.baseUrl(API_ENDPOINT)
.addConverterFactory(MoshiConverterFactory.create())
.client(OkHttpClient())
.build()
.create(ApiService::class.java)
}
}
Pokemon data class
#Parcelize
#JsonClass(generateAdapter = true)
#Entity
data class Pokemon(
#PrimaryKey val id: Int,
val name: String,
#ColumnInfo(name = "base_experience") val baseExperience: Int,
val height: Int,
#ColumnInfo(name = "is_default") val isDefault: Boolean,
val order: Int,
val weight: Int,
val sprites: PokemonSprites,
) : Parcelable
PokemonDao.kt
#Dao
interface PokemonDao {
#Query("SELECT * FROM namedapiresource")
fun getAll(): LiveData<List<NamedApiResource>>
#Query("SELECT * FROM pokemon WHERE name=:name")
fun getOne(name: String): LiveData<Pokemon>
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertAllNamedApiResources(vararg characters: NamedApiResource)
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertAllPokemons(vararg characters: Pokemon)
}
I would guess because of how your getPokemon is defined:
val disposable = emitSource(
// Attempts to get a pokemon from the database - presumably this does
// not exist at first so would return null first
pekemonDao.getOne(name).map {
it
}
)
// AFTER NULL IS RETURNED this tries to fetch from the API
val pokemon = ApiService.apiService.getPokemon(name)
try {
disposable.dispose()
pekemonDao.insertAllPokemons(pokemon)
// After fetching from API, finally returns a non-null
pekemonDao.getOne(name).map {
it
}
So maybe just get ride of the initial block?
val disposable = emitSource(
pekemonDao.getOne(name).map {
it
}
)
Ín my app's room database I have a table called movie_table. When the user clicks the item, it is saved to another table called favorite_table. I tried almost two days but I can't solve it. I am new to MVVM.
For better understanding please see the code:
FavoriteMovieDao.kt
#Dao
interface FavoriteMovieDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertFavMovie(favorite: Favorite)
#Query("SELECT * FROM favorite_table WHERE id LIKE :id")
suspend fun getFavoriteMovieById(id:Int): Favorite
#Query("SELECT * FROM favorite_table")
suspend fun getAllFavoriteMovie(): List<Favorite>
#Delete
suspend fun deleteFavorite(favorite: Favorite)
}
Repository.kt
class Repository(context: Context) {
private val favoriteMovieDao: FavoriteMovieDao = MovieDatabase.invoke(context).getFavoriteMovieDao()
suspend fun insertFavMovie(favorite: Favorite){
favoriteMovieDao.insertFavMovie(favorite)
}
MovieDetailsViewModel.kt
class MovieDetailsViewModel(private val repository: Repository):ViewModel() {
private val favMovieResponse:MutableLiveData<List<Favorite>> = MutableLiveData()
val actorResponse:MutableLiveData<List<Movie>> = MutableLiveData()
private val insertFavMovie:MutableLiveData<Favorite> = MutableLiveData()
fun actorDetail(){
viewModelScope.launch {
val actor = repository.getAllMovieDB()
actorResponse.value = actor
}
}
fun insertFavMovie(favorite: Favorite){
viewModelScope.launch {
//i can't insert this
val insertFav = repository.insertFavMovie(favorite)
insertFavMovie.value = insertFav
}
}
}
}
MovieDetailsActivity.kt
class MovieDetailsActivity : AppCompatActivity(){
private lateinit var actorViewModel: MovieDeatilsViewModel
private val actorAdapter by lazy { ActorAdapter() }
private var isFav: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_movie_details)
val repository = Repository(this)
val viewModelFactory = MovieDetailsViewModelFactory(repository)
actorViewModel = ViewModelProvider(this,viewModelFactory).get(MovieDeatilsViewModel::class.java)
setUpPostRecyclerView()
actorViewModel.actorDetail()
actorViewModel.actorResponse.observe(this, Observer {actorList ->
actorAdapter.setData(actorList)
})
initBundle()
favBtn.setOnClickListener {
if (isFav){
isFav = false
favBtn.supportImageTintList = ColorStateList.valueOf(Color.parseColor("#E4C1C1"))
Log.d("msg","Not Favorite!")
}else{
isFav = true
favBtn.supportImageTintList = ColorStateList.valueOf(Color.parseColor("#FFC107"))
Log.d("msg","Favorite!")
}
}
}
private fun initBundle() {
val bundle:Bundle? = intent.extras
movieTitle.text = bundle!!.getString("title")
directorbc.text = bundle.getString("director")
genre.text = bundle.getString("genre")
releaseYear.text = bundle.getInt("year").toString()
language.text = bundle.getString("language")
country.text = bundle.getString("country")
rating.text = bundle.getString("rating")
plotText.text = bundle.getString("plot")
//moviePosterD.setImageDrawable(bundle!!.getString("image"))
Glide.with(this).load(bundle.getString("image")).into(moviePosterD);
val player = SimpleExoPlayer.Builder(this).build()
player.preparePlayer(movieView)
player.setSource(applicationContext, "http://html5videoformatconverter.com/data/images/happyfit2.mp4")
}
private fun setUpPostRecyclerView(){
actorRecyclerview.adapter = actorAdapter
actorRecyclerview.layoutManager = LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false)
}
}
How can I insert it? Please help me. Thank you
In Movies table, to handle the favorite movies, you can make use of a variable in each movie item which you can for example set to true or false. This allows you to query using filters to show them on the screen. I don't think you need another table to store Favourites. I hope that helps! I can help you with the design if you share more details there.
#Entity
data class Movie( val title: String, val isFavourite: Boolean = false)
Update 1:
When user clicks on favButton, in MovieViewModel update the Movie entity like this:
fun setMovie(movie: Movie){
_movie.value = movie
}
fun updateMovie(movie: Movie) {
movie.isFavourite = !movie.isFavourite
viewModelScope.launch {
repository.updateMovie(movie)
setMovie(movie)
}
}
in MovieDao
#Update
suspend fun updateMovie(movie: Movie)
Update 2:
In MoviesDao
#Update
suspend fun updateMovie(movie: Movie)
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertAll(vararg movie: Movie)
#Query("select * from movies_table where isFavourite = 1")
fun getFavMovies(): LiveData<List<Movie>>
#Query("Select * from movies_table")
fun getAllMovies(): LiveData<List<Movie>>
In Repository
val favMovies: LiveData<List<Movie>> = moviesDao.getFavMovies()
Update 3:
favBtn.setOnClickListener {
_viewModel.updateMovie(movie)
}
_viewModel.movie.observe(viewLifecycleOwner, Observer {
//Update UI based on isFavourite value on Movie
})
I do not know how to implement a filter query properly inside the Repository and the ViewModel to use it to display the filtered string in a Textview or anything else really. My Entity, Dao, Repository, and ViewModel are as follows:
User.kt
#Entity(tableName = "user_data")
data class User (
#PrimaryKey(autoGenerate = true) val id: Int,
#ColumnInfo(name = "name") val name: String
)
UserDao.kt
#Dao
interface UserDao {
#Insert
fun addUser(user: User)
#Query("SELECT name FROM user_data WHERE name LIKE :filter LIMIT 1")
fun getFilteredUser(filter: String): LiveData<String>
}
UserRepository.kt
class UserRepository(private val userDao: UserDao) {
fun addUser(user: User) { userDao.addUser(User) }
fun getFilteredUser(filter: String) {userDao.getFilteredUser(filter)}
}
UserViewModel.kt
class UserViewModel(application: Application): AndroidViewModel(application) {
private val repository: UserRepository
init {
val userDao = UserDatabase.getDatabase(application).userDao()
repository = UserRepository(userDao )
}
fun addUser(user: User) {
viewModelScope.launch(Dispatchers.IO){
repository.addUser(user)
}
}
fun getFilteredUser(filter: String){
return repository.getFilteredUser(filter)
}
}
How would I proceed from here to make it possible to e.g. display the filtered User String in a textview or anything like that and how do I write the method correctly inside the repository and the viewmodel?
Thank you for your help!
Try the following
UserDao
change getFilteredUser as follows
#Query("SELECT name FROM user_data WHERE name LIKE '%' || :filter || '%' LIMIT 1")
fun getFilteredUser(filter: String): Stringl̥
UserRepo
use coroutines to perform the database I/O operations
suspend fun addUser(user: User) {
withContext(Dispatchers.IO) {
userDao.addUser(user)
}
}
suspend fun getFilteredUser(filter: String): String {
return withContext(Dispatchers.IO) {
userDao.getFilteredUser(filter)
}
}
ViewModel
fun addUser(user: User) {
viewModelScope.launch {
repository.addUser(user)
}
}
private val _dataToUi = MutableLiveData<String>()
val dataToUi: LiveData<String>
get() = _dataToUi
suspend fun getFilteredUser(filter: String): String? {
return withContext(Dispatchers.IO) {
repository.getFilteredUser(filter)
}
}
// to set the filterquery from the fragment/activity
fun setFliterQuery(query: String) {
viewModelScope.launch {
_dataToUi.value = getFilteredUser(query)
}
}
Activity
binding.button.setOnClickListener {
val queryKey = binding.queryKey.text.toString()
Log.i("activity", queryKey)
userViewModel.setFliterQuery(queryKey)
}
userViewModel.dataToUi.observe(this) { result ->
result?.apply {
Log.i("activity", result)
binding.resultText.text = result
}
}
i am implementing Room with a vIewModel, my structure is the following
#DAO,#Entity,#Database,#Repository
#Entity(tableName="dx_table")
class dx_table(
#ColumnInfo(name = "name")
val naxme: String,
#PrimaryKey
#ColumnInfo(name = "phone")
val phone: String,
#ColumnInfo(name = "passx")
val passx: String,
#ColumnInfo(name = "login_fx")
val login_fx: String
)
#Dao
interface dx_dao{
#Query("SELECT * FROM dx_table")
fun get_all():LiveData<List<dx_table>>
#Insert
suspend fun insertTrx(dx_table:dx_table)
#Query("UPDATE dx_table SET login_fx =:login_fx where phone=:phonex")
suspend fun insertFx(login_fx: String,phonex: String)
#Query("SELECT * from dx_table where phone=:phonex")
suspend fun get_name_px(phonex: String):List<dx_table>
#Query("Delete from dx_table")
suspend fun deleteAll()
#Query("Select * from dx_table where login_fx=1")
suspend fun selectFx():List<dx_table>
}
#Database(entities = arrayOf(dx_table::class), version = 1, exportSchema = false)
public abstract class DxDatabase : RoomDatabase() {
abstract fun dxDao(): dx_dao
companion object {
// Singleton prevents multiple instances of database opening at the
// same time.
#Volatile
private var INSTANCE: DxDatabase? = null
fun getDatabase(context: Context): DxDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
DxDatabase::class.java,
"dx_database"
).build()
INSTANCE = instance
return instance
}
}
}
}
class dxRepository(private val dxDao: dx_dao ){
val k_d:LiveData<List<dx_table>> = dxDao.get_all()
suspend fun insert_trx(dx_table: dx_table){
dxDao.insertTrx(dx_table)
}
suspend fun insert_fx(login_fx: String,phonex: String) {
dxDao.insertFx(login_fx,phonex)
}
suspend fun select_fx() {
dxDao.selectFx()
}
suspend fun get_name_px(phonex: String) :List<dx_table> {
return dxDao.get_name_px(phonex) as List<dx_table>
}
}
The viewmodel is
class DxViewModel (application: Application) : AndroidViewModel(application) {
var repository: dxRepository
var k_d: LiveData<List<dx_table>>
init {
// Gets reference to WordDao from WordRoomDatabase to construct
// the correct WordRepository.
val dxDao = DxDatabase.getDatabase(application).dxDao()
repository = dxRepository(dxDao)
k_d = repository.k_d
}
fun insert_trx(dxTable: dx_table) = viewModelScope.launch {
repository.insert_trx(dxTable)
}
fun insert_fx(login_fx: String, phonex: String) = viewModelScope.launch {
repository.insert_fx(login_fx, phonex)
}
fun select_fx() = viewModelScope.launch {
repository.select_fx()
}
fun get_name_px(phonex: String) = viewModelScope.launch {
repository.get_name_px(phonex)
}
}
I can track the live data using observe,its not an issue, the problem i am facing is with the get_name_px(phone)
var mView = ViewModelProviders.of(this).get(DxViewModel::class.java)
var lm = mView.get_name_px(phone)
here lm seems to be job type , i need the return List , how do i get it .
In your viewModel function select_fx() return a job, because launch does not return result, so you have to either:
1) Use async and await
fun get_name_px(phonex: String) = viewModelScope.async {
repository.get_name_px(phonex)
}.await()
2) Not use launch viewModel, use it in Activity/Fragment
suspend fun get_name_px(phonex: String) = repository.get_name_px(phonex)
class CardFragment : Fragment() {
fun get() {
// launch in Dispatchers.Main
lifecycleScope.launch {
var lm = mView.get_name_px(phone)
}
// launch in background thread
lifecycleScope.launch(Dispatchers.Default) {
var lm = mView.get_name_px(phone)
}
}
}
I'd like to get a User from my Room database using the Repository, and then observing it in the OverviewFragment.
What I tried:
using an Observer in the fragment, but I got a NullPointerException.
using #{overviewViewModel.user.email} in fragment_overview.xml: the application runs but nothing is displayed.
Unit Testing and Instrumented Testing with a FakeRepository. The tests pass and the data are displayed.
What I use:
Android Studio (up to date)
Kotlin
MVVM architecture
Repository pattern linked to a Room database
Constructor dependency injection (for the viewmodels)
ServiceLocator pattern for testing
User Flow (Navigation)
The User opens the app, fills a form and clicks the registration button in the RegistrationFragment, then navigates to the OverviewFragment, where his data (Email, Password) are displayed.
RegistrationViewModel:
class RegistrationViewModel(private val repository: IUserRepository) : ViewModel() {
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private val _user = MutableLiveData<User>()
val user: LiveData<User>
get() = _user
val email = MutableLiveData<String>()
val password = MutableLiveData<String>()
[...]
fun onRegister() {
val userEmail = email.value.toString()
val userPassword = password.value.toString()
registerUser(User(userEmail, userPassword))
}
private fun registerUser(newUser: User) = uiScope.launch {
repository.registerUser(newUser)
}
[...]
}
OverviewViewModel
class OverviewViewModel(val database: UserDatabaseDao,
private val userRepository: IUserRepository
) : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User>
get() = _user
init {
getUser()
}
private fun getUser() {
runBlocking {_user.value = userRepository.getUser().value }
}
OverviewFragment:
class OverviewFragment : Fragment() {
[...]
overviewViewModel.user.observe(this, Observer {
binding.apply {
textview.text = overviewViewModel.user.value!!.email
textview2.text = overviewViewModel.user.value!!.password
}
})
return binding.root
}
}
Repository:
class UserRepository(private val dataSource: UserDatabaseDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) :
IUserRepository {
override suspend fun registerUser(user: User) = withContext(ioDispatcher) {
dataSource.insert(user)
}
override suspend fun getUser(): LiveData<User?> = withContext(ioDispatcher) {
return#withContext dataSource.getUser()
}
override suspend fun getUser(email: String): LiveData<User?> = withContext(ioDispatcher) {
return#withContext dataSource.getUser(email)
}
}
DatabaseDao:
#Dao
interface UserDatabaseDao {
#Insert
fun insert(user: User)
#Query("SELECT * from user_table WHERE email = :key")
fun getUser(key: String): LiveData<User?>
#Query("SELECT * from user_table")
fun getUser(): LiveData<User?>
#Query("SELECT * from user_table")
fun getAllUsers(): LiveData<List<User>>
}
Note: there's only one user in the database, the one that fills the form in the RegistrationFragment.