Live data observer not triggered after room db query - android

I have a LiveData object which i'm observing within a fragment within an activity. The activity listens to broadcasts from the system via 2 broadcast receivers. The data is queried from room according to the current date. So the receivers detect if the date was changed and if so, go and refresh the query.
Problem is after i change the date in the settings and come back to the app, the observer isn't triggered. If i go to another fragment and return the data will change, but not from the
Now for the code:
Activity:
private lateinit var timeReceiver: MyTimeReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
timeReceiver = MyTimeReceiver()
registerReceiver(timeReceiver, filter)
registerReceiver(refreshReceiver, IntentFilter("refresh_data"))
mainFragment = MainFragment.newInstance()
}
private val refreshReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == "refresh_data") {
Timber.e("time changed, need to refresh data")
mainFragment.refreshData()
}
}
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(timeReceiver)
unregisterReceiver(refreshReceiver)
}
Fragment:
private var counter: Int = 0
private lateinit var viewModel: MainFragmentViewModel
private lateinit var date: String
companion object {
fun newInstance(): MainFragment {
return MainFragment()
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_main, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
plus_btn.setOnClickListener { addCup() }
minus_btn.setOnClickListener { decreaseCup() }
viewModel = ViewModelProviders.of(this).get(MainFragmentViewModel::class.java)
viewModel.dataList.observe(viewLifecycleOwner, Observer {
it.let { it1 ->
counter = it1.size
cup_counter.text = "$counter"
setGoalVisibility()
}
})
}
override fun onResume() {
super.onResume()
viewModel.checkCups(setDate())
}
private fun decreaseCup() {
viewModel.deleteCup(counter, date)
}
private fun addCup() {
Timber.e(date)
viewModel.insertCup(
WaterCupEntity(
System.currentTimeMillis(),
++counter,
date
)
)
}
fun refreshData() {
viewModel.checkCups(setDate())
}
private fun setDate(): String {
val dateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.US)
date = dateFormat.format(Calendar.getInstance().time)
return date
}
ViewModel:
class MainFragmentViewModel(application: Application) : AndroidViewModel(application) {
private var dao: WaterCupDao
var dataList: LiveData<List<WaterCupEntity>>
private var job = Job()
private val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private val scope = CoroutineScope(coroutineContext)
private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.US)
var date: String
init {
dao = MyCupsDb.getInstance(application, scope).getDao()
date = dateFormat.format(Calendar.getInstance().time)
Timber.e("getting all cups for date: %s", date)
dataList = dao.getAllCupsWithSameDate(date)
}
fun checkCups(date :String) {
dataList = dao.getAllCupsWithSameDate(date)
}
fun insertCup(cup: WaterCupEntity) = runBlocking {
scope.launch(Dispatchers.IO) {
dao.insert(cup)
}
}
fun deleteCup(number: Int, date: String) =
scope.launch(Dispatchers.IO) {
dao.deleteCupForDate(number, date)
}
fun deleteToday(date :String) {
scope.launch (Dispatchers.IO){
dao.deleteAllCupsForDate(date)
}
}
fun deleteAllCups() {
scope.launch (Dispatchers.IO){
dao.deleteAllCups()
}
}
}
Room db :
#Database(entities = [WaterCupEntity::class], version = 1)
abstract class MyCupsDb : RoomDatabase() {
abstract fun getDao(): WaterCupDao
companion object {
#Volatile
private var instance: MyCupsDb? = null
fun getInstance(context: Context, scope:CoroutineScope): MyCupsDb {
return instance ?: synchronized(this) {
instance ?: buildDb(context,scope).also { instance = it }
}
}
private fun buildDb(context: Context,scope:CoroutineScope): MyCupsDb {
return Room.databaseBuilder(context, MyCupsDb::class.java, "myDb")
.addCallback(WordDatabaseCallback(scope))
.build()
}
}
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onOpen(db)
instance?.let { database ->
scope.launch(Dispatchers.IO) {
populateDatabase(database.getDao())
}
}
}
fun populateDatabase(dao: WaterCupDao) {
var word = WaterCupEntity(System.currentTimeMillis(),1,"11.01.2019")
dao.insert(word)
word = WaterCupEntity(System.currentTimeMillis(),2,"11.01.2019")
dao.insert(word)
}
}
}
MyTimeReceiver:
class MyTimeReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action){
Intent.ACTION_TIMEZONE_CHANGED,
Intent.ACTION_DATE_CHANGED,
Intent.ACTION_TIME_CHANGED,
Intent.ACTION_TIME_TICK -> context.sendBroadcast(Intent("refresh_data"))
}
}
}

So mainly the use case was to trigger the LiveData query after some event - in this case an intent from a BroadcastReceiver - so it is refreshed.
I wanted to put new params for the query and then make the query work.
What i didn't realize is this is basically against the design of LiveData. LiveData is data that you subscribe to, a client that subscribes to that data doesn't affect it, it merely listens to its changes.
So the problem here is with design.
To get a new query you usually need either a new data (so your observe will trigger) or resubscribe to the LiveData - the harder and more complicated approach since then you need to manage the subscriptions so you don't leak.
I chose to get a new data via using an insert once i get an intent. If anyone so wishes i can post the fixed code for this.

Related

Combining Two Flows and add them to temporary list of StateFlow(Hot Flows)

I m getting data from two End points using flows and assigning those two list to temporary list in ViewModel. For this purpose, I'm using combine function and returning result as stateFlows with stateIn operator but that's not working. Can anyone point me out where I go wrong please.
ViewModel.kt
private val _movieItem: MutableStateFlow<State<List<HomeRecyclerViewItems>>> =
MutableStateFlow(State.Loading())
val movieItems: StateFlow<State<List<HomeRecyclerViewItems>>> = _movieItem
fun getHomeItemList() {
viewModelScope.launch {
val testList: Flow<State<List<HomeRecyclerViewItems.Movie>>> =
settingsRepo.getMovieList().map {
State.fromResource(it)
}
val directorList: Flow<State<List<HomeRecyclerViewItems.Directors>>> =
settingsRepo.getDirectorList().map {
State.fromResource(it)
}
_movieItem.value = combine(testList, directorList) { testList, directorList ->
testList + directorList // This is not working as "+" Unresolve Error
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
State.loading<Nothing>()
) as State<List<HomeRecyclerViewItems>> // Unchecked cast: StateFlow<Any> to State<List<HomeRecyclerViewItems>>
}
Repository.kt
fun getMovieList(): Flow<ResponseAPI<List<HomeRecyclerViewItems.Movie>>> {
return object :
NetworkBoundRepository<List<HomeRecyclerViewItems.Movie>, List<HomeRecyclerViewItems.Movie>>() {
override suspend fun saveRemoteData(response: List<HomeRecyclerViewItems.Movie>) {
}
override fun fetchFromLocal() {
}
override suspend fun fetchFromRemote(): Response<List<HomeRecyclerViewItems.Movie>> =
apiInterface.getMoviesList()
}.asFlow()
}
fun getDirectorList(): Flow<ResponseAPI<List<HomeRecyclerViewItems.Directors>>> {
return object :
NetworkBoundRepository<List<HomeRecyclerViewItems.Directors>, List<HomeRecyclerViewItems.Directors>>() {
override suspend fun saveRemoteData(response: List<HomeRecyclerViewItems.Directors>) {
}
override fun fetchFromLocal() {
}
override suspend fun fetchFromRemote(): Response<List<HomeRecyclerViewItems.Directors>> =
apiInterface.getDirectorsList()
}.asFlow()
}
Network BoundRepository.kt
#ExperimentalCoroutinesApi
abstract class NetworkBoundRepository<RESULT, REQUEST> {
fun asFlow() = flow<ResponseAPI<REQUEST>> {
val apiResponse = fetchFromRemote()
val remotePosts = apiResponse.body()
if (apiResponse.isSuccessful && remotePosts != null) {
emit(ResponseAPI.Success(remotePosts))
} else {
emit(ResponseAPI.Failed(apiResponse.errorBody()!!.string()))
}
}.catch { e ->
e.printStackTrace()
emit(ResponseAPI.Failed("Server Problem! Please try again Later. "))
}
#WorkerThread
protected abstract suspend fun saveRemoteData(response: REQUEST)
#MainThread
protected abstract fun fetchFromLocal()
#MainThread
protected abstract suspend fun fetchFromRemote(): Response<REQUEST>
}
Endpoints with Sealed Class
#GET("directors")
fun getDirectorsList(): Response<List<HomeRecyclerViewItems.Directors>>
#GET("movies")
fun getMoviesList(): Response<List<HomeRecyclerViewItems.Movie>>
sealed class HomeRecyclerViewItems {
class Title(
val id: Int,
val title: String
) : HomeRecyclerViewItems()
class Movie(
val id: Int,
val title: String,
val thumbnail: String,
val releaseDate: String
) : HomeRecyclerViewItems()
class Directors(
val id: Int,
val name: String,
val avator: String,
val movie_count: Int
) : HomeRecyclerViewItems()
}
Fragment.kt
#AndroidEntryPoint
#ExperimentalCoroutinesApi
class SettingsFragment : BaseBottomTabFragment() {
private var _binding: FragmentSettingsBinding? = null
private val binding get() = _binding!!
private val viewModel by viewModels<SettingViewModel>()
#Inject
lateinit var recyclerViewAdapter: RecyclerViewAdapter
#Inject
lateinit var bundle: Bundle
var finalList = mutableListOf<HomeRecyclerViewItems>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentSettingsBinding.inflate(layoutInflater,container,false)
val view = binding.root
binding.rvMovie.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(activity)
}
bundle.putString("Hello","hihg")
Toast.makeText(activity, "${bundle.getString("Hello")}", Toast.LENGTH_SHORT).show()
finalList.add(HomeRecyclerViewItems.Title(1,"hello"))
return view
}
private fun observeList() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED){
launch {
viewModel.movieItems.collect { state ->
when(state){
is State.Loading ->{
}
is State.Success->{
if (state.data.isNotEmpty()){
recyclerViewAdapter = RecyclerViewAdapter()
binding.rvMovie.adapter = recyclerViewAdapter
recyclerViewAdapter.submitList(finalList)
}
}
is State.Error -> {
Toast.makeText(activity, "Error", Toast.LENGTH_SHORT).show()
}
else -> Unit
}
}
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
(activity as MainActivity).binding.ivSearch.isGone = true
viewModel.getHomeItemList()
observeList()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Note: I m following this tutorial simpliedCoding for api data for multirecyclerview but want to implement it with Kotlin State Flow. Any help in this regard is highly appreciated. Thanks.
Your problem is in here
val testList: Flow<State<List<HomeRecyclerViewItems.Movie>>> =
settingsRepo.getMovieList().map {
State.fromResource(it)
}
val directorList: Flow<State<List<HomeRecyclerViewItems.Directors>>> =
settingsRepo.getDirectorList().map {
State.fromResource(it)
}
_movieItem.value = combine(testList, directorList) { testList, directorList ->
testList + directorList
}
They are not returning a List<HomeRecyclerViewItems>, but a State<List<HomeRecyclerViewItems>. Maybe a better name for the variables are testsState and directorsState. After that it will be more clear why you need to unpack the values before combining the lists
_movieItem.value = combine(testsState, directorsState) { testsState, directorsState ->
val homeRecyclerViewItems = mutableListOf<HomeRecyclerViewItems>()
if (testsState is Success) homeRecyclerViewItems.add(testsState.data)
if (directorsState is Success) homeRecyclerViewItems.add(directorsState.data)
homeRecyclerViewItems
}

How to show query to database in ListView in kotlin with android studio?

I am learning to develop in kotlin with android studio, so I don't have any experience.
I am wanting to insert data into a local database using room database, so far I think I'm doing fine. Now I need to be able to consult those data but I cannot do it, I have searched the internet but I have not been able to solve my problem.
I attach the code.
Class #Entity
class TablasBdApp {
#Entity(tableName = TblConteo.TABLE_NAME)
data class TblConteo(
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "linea_id") val Linea_Id: Int = 0,
#ColumnInfo (name = "articulo") val Articulo : String?
)
{
companion object {
const val TABLE_NAME = "TablaConteo"
}
}
}
Class #Dao
#Dao
public interface ItblConteoDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertArticulo(taskTblConteo: TablasBdApp.TblConteo);
#Query("SELECT * FROM " + TablasBdApp.TblConteo.TABLE_NAME + " ORDER BY Linea_Id desc")
fun getConteoArticulos(): LiveData<List<TablasBdApp.TblConteo>>
}
Class #DataBase
class BaseDeDatos {
#Database(entities = [TablasBdApp.TblConteo::class], version = 1)
abstract class PortatilDataBase : RoomDatabase() {
abstract fun itblconteoDao () : ItblConteoDao
companion object {
private const val DATABASE_NAME = "portatildb"
#Volatile
private var INSTANCE: PortatilDataBase? = null
fun getInstance(context: Context): PortatilDataBase? {
INSTANCE ?: synchronized(this) {
INSTANCE = Room.databaseBuilder(
context.applicationContext,
PortatilDataBase::class.java,
DATABASE_NAME
).build()
}
return INSTANCE
}
}
}
}
Class #Repository
class ConteoRepository(application: Application) {
private val itblConteoDao: ItblConteoDao? = BaseDeDatos.PortatilDataBase.getInstance(application)?.itblconteoDao()
fun insert (tblconteo: TablasBdApp.TblConteo){
if(itblConteoDao != null) InsertAsyncTask(itblConteoDao).execute(tblconteo)
}
fun getConteo(): LiveData<List<TablasBdApp.TblConteo>> {
return itblConteoDao?.getConteoArticulos() ?: MutableLiveData<List<TablasBdApp.TblConteo>>()
}
private class InsertAsyncTask(private val itblConteoDao: ItblConteoDao) :
AsyncTask<TablasBdApp.TblConteo, Void, Void>() {
override fun doInBackground(vararg tblconteos: TablasBdApp.TblConteo?): Void? {
for (tblconteo in tblconteos) {
if (tblconteo != null) itblConteoDao.insertArticulo(tblconteo)
}
return null
}
}
}
Class #CustomAdapter
class CustomAdapter(context: Context): BaseAdapter() {
private val mContext: Context
init{
mContext = context
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
//val repository = ConteoRepository(application = Application())
var textView = TextView(mContext)
val observer = Observer<List<TablasBdApp.TblConteo>> { conteos ->
if (conteos != null) {
var text = ""
for (conteo in conteos) {
text += conteo.Linea_Id.toString() + " " + conteo.Articulo
}
textView.text = text
}
}
//repository.getConteo().observe(this, observer)
return textView
}
override fun getItem(position: Int): Any {
return "Test Articulo"
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getCount(): Int {
TODO("Not yet implemented")
}
}
Class #Fragment
class CapturaConteoFragment() : Fragment() {
private lateinit var capturaConteoViewModel: CapturaConteoViewModel
#SuppressLint("ResourceType")
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
capturaConteoViewModel =
ViewModelProviders.of(this).get(CapturaConteoViewModel::class.java)
val root = inflater.inflate(R.layout.fragment_capturaconteo, container, false)
//val textView: TextView = root.findViewById(R.id.text_capturaconteo)
capturaConteoViewModel.text.observe(viewLifecycleOwner, Observer {
//textView.text = it
val botonGuardar: Button = root.findViewById(R.id.btn_guardar)
val textoArticulo : EditText = root.findViewById(R.id.edit_art_upc)
var listview: ListView = root.findViewById(R.id.list_conteo)
botonGuardar.setOnClickListener {
if(textoArticulo.getText().toString().trim().isEmpty()){
Toast.makeText(activity, "Captura un articulo", Toast.LENGTH_LONG).show()
textoArticulo.requestFocus()
}
else{
saveConteo(TablasBdApp.TblConteo(Articulo = textoArticulo.text.trim().toString()))
Toast.makeText(activity, "Articulo guardado", Toast.LENGTH_LONG).show()
//var listaconteo = arrayOf(muestraConteo())
var prodAdapter = CustomAdapter(**this**) **<- This is where it marks error.**
listview?.adapter = prodAdapter
}
}
})
return root
}
private fun saveConteo(tblConteo: TablasBdApp.TblConteo) {
val repository = ConteoRepository(application = Application())
repository.insert(tblConteo)
val conteo = repository.getConteo()
}
private fun showConteo (){
val repository = ConteoRepository(application = Application())
repository.getConteo()
}
}
Thanks for your help.
Regards.
In your DAO you are returning LiveData that means you need to observe it and best practice would be through ViewModel which implies you should MVVM architecture, there are plenty of tutorials and courses on it.
Here is something from google Codelabs
Other way would would be to return just list rather than live data of a list. This is bad practice because you will have to refresh it all the time and do a lot of extra work.
PS your adapter needs fixing too my suggestion is follow codelabs or watch complete tutorial on Room + MVVM.
Hope this helps

Room SQLite query doesn't return any results, even though there are rows in the database that match

I am using this command to get data from a Room database:
select * from location_search_results where searchQuery = "wilmington"
Here is what the database looks like:
And here are the search results:
I have verified that the name of the table is correct and everything, and there's no spelling errors that I can find, so why would this query not return any of the three rows that it should match?
EDIT for code:
The source code is publicly available here, I am working in the mvvm branch, so if you pull it, make sure you're there. Below are the relevant classes:
LocationSearchResponse.kt:
#Entity(tableName = "location_search_results")
class LocationSearchResponse(
#ColumnInfo(name = "type")
val type: String,
#TypeConverters(DataConverter::class)
#SerializedName("query")
val searchQuery: List<String>,
#TypeConverters(DataConverter::class)
val features: List<Feature>,
#ColumnInfo(name = "attribution")
val attribution: String
) {
#PrimaryKey(autoGenerate = true)
var id: Int = 0
}
LocationSearchRepositoryImpl.kt:
class LocationSearchRepositoryImpl (
private val locationResponseDao: LocationResponseDao,
private val locationNetworkDataSource: LocationNetworkDataSource
): LocationSearchRepository {
init {
locationNetworkDataSource.downloadedLocationSearchResults.observeForever { locationResults ->
persistFetchedLocations(locationResults)
}
}
// update search data in db if necessary, then return the data that was searched for.
override suspend fun searchForLocation(query: String): LiveData<out LocationSearchResponse> {
return withContext(Dispatchers.IO) {
initSearch(query)
return#withContext locationResponseDao.searchForLocation(query)
}
}
// if a fetch is necessary (query has not already been searched), fetch search results
private suspend fun initSearch(query: String) {
if (isFetchLocationResultsNeeded(query))
fetchLocationResults(query)
}
private fun isFetchLocationResultsNeeded(query: String) : Boolean {
// get the cached results. If it's null, return true because it needs to be updated
val cachedResults = locationResponseDao.searchForLocationNonLive(query.toLowerCase())
if (cachedResults == null) return true
// if the results are empty, it needs to be fetched, else it doesn't
return cachedResults.features.isEmpty()
}
private suspend fun fetchLocationResults(query: String) {
locationNetworkDataSource.fetchLocationSearchResults("mapbox.places", query)
}
private fun persistFetchedLocations(fetchedLocationResults: LocationSearchResponse) {
GlobalScope.launch(Dispatchers.IO) {
locationResponseDao.upsert(fetchedLocationResults)
}
}
}
LocationResponseDao.kt:
#Dao
interface LocationResponseDao {
// update or insert existing entry if there is a conflict when adding to db
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun upsert(locationResults: LocationSearchResponse)
#Query("select * from location_search_results WHERE searchQuery = :query")
fun searchForLocation(query: String): LiveData<LocationSearchResponse>
#Query("select * from location_search_results WHERE searchQuery = :query")
fun searchForLocationNonLive(query: String): LocationSearchResponse?
#Query("delete from location_search_results")
fun nukeTable()
}
and ChooseCityFragment.kt:
class ChooseCityFragment : ScopedFragment(), KodeinAware {
override val kodein by closestKodein()
private val locationViewModelFactory: LocationResponseViewModelFactory by instance()
private val weatherResponseViewModelFactory: WeatherResponseViewModelFactory by instance()
private lateinit var locationViewModel: LocationResponseViewModel
private lateinit var weatherViewModel: WeatherResponseViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
setupViews()
// Inflate the layout for this fragment
return inflater.inflate(R.layout.choose_city_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
locationViewModel = ViewModelProviders.of(this, locationViewModelFactory)
.get(LocationResponseViewModel::class.java)
weatherViewModel = ViewModelProviders.of(this, weatherResponseViewModelFactory)
.get(WeatherResponseViewModel::class.java)
updateToolbar()
}
fun updateToolbar() {
(activity as? AppCompatActivity)?.supportActionBar?.title = "Choose Location"
(activity as? AppCompatActivity)?.supportActionBar?.subtitle = null
}
fun bindUI() = launch {
val locationResults = locationViewModel.locationResponse
val owner = viewLifecycleOwner
locationResults.observe(owner, Observer {
if (it == null) return#Observer
// TODO: set loading icon to GONE
initRecyclerView(it.features.toLocationSearchResultListItem())
})
}
fun setupViews() = launch {
search_button.setOnClickListener {
searchLocations()
search_results_rv.adapter?.notifyDataSetChanged()
}
}
// TODO: search text can not be more than 20 words or more than 256 characters. Need to account for this
fun searchLocations() = launch {
val searchText = search_box.text.toString()
if (searchText != "") {
locationViewModel.searchLocation(search_box.text.toString())
bindUI()
} else
Toast.makeText(context?.applicationContext, "Please enter a search term", Toast.LENGTH_SHORT).show()
}
private fun List<Feature>.toLocationSearchResultListItem() : List<LocationSearchResultListItem> {
return this.map {
LocationSearchResultListItem(it)
}
}
private fun initRecyclerView(items: List<LocationSearchResultListItem>) {
val groupAdapter = GroupAdapter<ViewHolder>().apply {
addAll(items)
}
groupAdapter.notifyDataSetChanged()
search_results_rv.apply {
layoutManager = LinearLayoutManager(this#ChooseCityFragment.context)
adapter = groupAdapter
}
groupAdapter.setOnItemClickListener { item, view ->
(item as? LocationSearchResultListItem)?.let {
refreshWeather(it.feature.coordinates[0], it.feature.coordinates[1])
}
}
}
private fun refreshWeather(latitude: Double, longitude: Double) = launch {
weatherViewModel.refreshWeatherWithCoordinates(latitude, longitude)
}
}
It turns out there was a space being added to the end of the searchQuery that I wasn't able to see. Once I figured out where my code was adding that space, I removed it and now everything looks good.
try something like this
it`s a dao interface example
use big letters in your query
#Query("SELECT * FROM person WHERE favoriteColor LIKE :color")
List<Person> getAllPeopleWithFavoriteColor(String color);
more info here

Android Coroutines block UI click listener

I'm using MVVM as architecture, also the repository pattern. I have a Web service, a room database also. Using coroutines block any button I click.
There's a list/detail implemented with a fragment and an activity respectively.
I can figure out what's wrong in the way I implemented the coroutines and Viewmodel.
class BuySharedViewModel(application: Application) : AndroidViewModel(application) {
private val repository: BuyRepository
var allBuys: LiveData<List<Buy>>
init {
val buyDao = KunukRoomDatabase.getDatabase(application, viewModelScope).buyDao()
val buyRemote = BuyRemote()
repository = BuyRepository.getInstance(buyDao , buyRemote)
//Use async because it return a result
viewModelScope.launch { getAllBuys() }
allBuys = buyDao.loadAllBuys()
}
private suspend fun getAllBuys() {
repository.getBuys()
}
}
Here's is the Repository, it take data from web service and add it to the room database, while ViewModel get's data from room database.
class BuyRepository (private val buyDao: BuyDao, private val buyRemote: BuyRemote) {
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.Default + job)
companion object {
//For singleton instantiation
#Volatile private var instance: BuyRepository? = null
fun getInstance(buyDao: BuyDao, buyRemote: BuyRemote) =
instance ?: synchronized(this) {
instance ?: BuyRepository(buyDao, buyRemote)
.also { instance = it}
}
}
suspend fun getBuys(){
refresh()
}
private suspend fun refresh(){
try {
val list = scope.async { buyRemote.loadBuys() }
list.await().forEach { buy -> insert(buy) }
} catch (e: Throwable) {}
}
#WorkerThread
private fun insert(buy: Buy) {
buyDao.insertBuy(buy)
}
}
The fragment work, data are displayed, when i click on an item from that fragment(recyclerView) it work, the activity display details data. But none of the click on that activity works, like it doesn't detect the clicks. I guess it got something to do with the coroutines because when I comment out the code viewmodelScope.launch { getAllBuys()} from the BuySharedViewModel it works, because it load data from the previous call from room database, and the clicks works.
Here's the code in the detail view:
class BuyDetailActivity : AppCompatActivity() {
private lateinit var sharedViewModel: BuySharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lateinit var buy: Buy
sharedViewModel = ViewModelProviders.of(this).get(BuySharedViewModel::class.java)
val position = intent.getIntExtra("position", 0)
sharedViewModel.allBuys.observe(this, Observer<List<Buy>> { buys ->
buy = buys[position]
val binding: com.example.drake.kunuk.databinding.ActivityBuyDetailBinding =
DataBindingUtil.setContentView(this, com.example.drake.kunuk.R.layout.activity_buy_detail)
binding.buy = buy
val agentNumber = buy.agentNumber?:"+50937438713"
bnvContactAgent.setOnNavigationItemSelectedListener { item ->
when (item.itemId) {
com.example.drake.kunuk.R.id.action_call -> {
val callNumberUri = Uri.parse("tel:$agentNumber")
val callIntent = Intent(Intent.ACTION_DIAL, callNumberUri)
startActivity(callIntent)
}
com.example.drake.kunuk.R.id.action_sms -> {
val smsNumberUri = Uri.parse("sms:$agentNumber")
val smsIntent = Intent(Intent.ACTION_SENDTO, smsNumberUri)
startActivity(smsIntent)
}
com.example.drake.kunuk.R.id.action_email -> {
val uriText = "mailto:drakecolin#gmail.com" +
"?subject=" + Uri.encode("I'm interested in $agentNumber") +
"&body=" + Uri.encode("Hello, ")
val uri = Uri.parse(uriText)
val sendIntent = Intent(Intent.ACTION_SENDTO)
sendIntent.data = uri
startActivity(Intent.createChooser(sendIntent, "Send email"))
}
}
false
}
This is the code of my fragment:
class BuyFragment : Fragment() {
companion object {
fun newInstance() = BuyFragment()
}
private lateinit var viewModel: BuySharedViewModel
private val buyList = ArrayList<Buy>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Get a new or existing ViewModel from the ViewModelProvider.
viewModel = ViewModelProviders.of(this).get(BuySharedViewModel::class.java)
// Add an observer on the LiveData returned by loadAllBuys.
// The onChanged() method fires when the observed data changes and the activity is
// in the foreground.
viewModel.allBuys.observe(this, Observer<List<Buy>> { buys ->
// Update the cached copy of the words in the adapter.
buys?.let { (rvBuy.adapter as BuyAdapter).setBuys(it) }
progressBar.visibility = View.GONE
})
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.buy_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
rvBuy.layoutManager = LinearLayoutManager(context)
rvBuy.adapter = BuyAdapter(activity!!.applicationContext,
R.layout.buy_card, buyList)
progressBar.visibility = View.VISIBLE
}
}
This is the code for the BuyDao:
#Dao
interface BuyDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertBuy(vararg buys: Buy)
#Update
fun updateBuy(vararg buys: Buy)
#Delete
fun deleteBuys(vararg buys: Buy)
#Query("SELECT * FROM buys")
fun loadAllBuys(): LiveData<List<Buy>>
#Query("DELETE FROM buys")
suspend fun deleteAll()
}
viewModelScope by default uses Dispatchers.Main and it is blocking your UI.
Try this:
viewmodelScope.launch(Dispatchers.IO) { getAllBuys()}
Edit:
The problem is your setting listner on BottomNavigation when your livedata is updated which is causing this weird issue.
Replace your BuyDetailActivity code with this:
class BuyDetailActivity : AppCompatActivity() {
private lateinit var sharedViewModel: BuySharedViewModel
private var agentNumber = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityBuyDetailBinding =
DataBindingUtil.setContentView(this, R.layout.activity_buy_detail)
binding.buy = Buy()
lateinit var buy: Buy
sharedViewModel = ViewModelProviders.of(this).get(BuySharedViewModel::class.java)
val position = intent.getIntExtra("position", 0)
sharedViewModel.allBuys.observe(this, Observer<List<Buy>> { buys ->
buy = buys[position]
binding.buy = buy
binding.executePendingBindings()
agentNumber = buy.agentNumber
// set animation duration via code, but preferable in your layout files by using the animation_duration attribute
expandableTextView.setAnimationDuration(750L)
// set interpolators for both expanding and collapsing animations
expandableTextView.setInterpolator(OvershootInterpolator())
// or set them separately.
expandableTextView.expandInterpolator = OvershootInterpolator()
expandableTextView.collapseInterpolator = OvershootInterpolator()
// toggle the ExpandableTextView
buttonToggle.setOnClickListener {
buttonToggle.setText(if (expandableTextView.isExpanded) com.example.drake.kunuk.R.string.more else com.example.drake.kunuk.R.string.less)
expandableTextView.toggle()
}
// but, you can also do the checks yourself
buttonToggle.setOnClickListener {
if (expandableTextView.isExpanded) {
expandableTextView.collapse()
buttonToggle.setText(com.example.drake.kunuk.R.string.more)
} else {
expandableTextView.expand()
buttonToggle.setText(com.example.drake.kunuk.R.string.less)
}
}
//Open photoView activity when clicked
ivHouseDetail.setOnClickListener {
applicationContext
.startActivity(
Intent(
applicationContext,
ViewPagerActivity::class.java
)
.putExtra("imageList", buy.propertyImage)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
}
})
findViewById<BottomNavigationView>(R.id.bnvContactAgent)?.setOnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.action_call -> {
Log.e("BIRJU", "Action call")
val callNumberUri = Uri.parse("tel:$agentNumber")
val callIntent = Intent(Intent.ACTION_DIAL, callNumberUri)
startActivity(callIntent)
}
R.id.action_sms -> {
Log.e("BIRJU", "Action SMS")
val smsNumberUri = Uri.parse("sms:$agentNumber")
val smsIntent = Intent(Intent.ACTION_SENDTO, smsNumberUri)
startActivity(smsIntent)
}
R.id.action_email -> {
Log.e("BIRJU", "Action Email")
val uriText = "mailto:drakecolin#gmail.com" +
"?subject=" + Uri.encode("I'm interested in $agentNumber") +
"&body=" + Uri.encode("Hello, ")
val uri = Uri.parse(uriText)
val sendIntent = Intent(Intent.ACTION_SENDTO)
sendIntent.data = uri
startActivity(Intent.createChooser(sendIntent, "Send email"))
}
}
false
}
}
}

Reload RecyclerView after data change with Room, ViewModel and LiveData

I am trying, without success, to solve a problem for days. I would like to update my recyclerView whenever the records of a particular model change in the Database (DB Room). I use ViewModel to handle the model data and the list of records are stored in LiveData.
Database
#Database(entities = arrayOf(Additive::class), version = ElementDatabase.DB_VERSION, exportSchema = false)
abstract class ElementDatabase() : RoomDatabase() {
companion object {
const val DB_NAME : String = "element_db"
const val DB_VERSION : Int = 1
fun get(appContext : Context) : ElementDatabase {
return Room.databaseBuilder(appContext, ElementDatabase::class.java, DB_NAME).build()
}
}
abstract fun additivesModels() : AdditiveDao
}
Model
#Entity
class Additive {
#PrimaryKey #ColumnInfo(name = "id")
var number : String = ""
var dangerousness : Int = 0
var description : String = ""
var names : String = ""
var notes : String = ""
var risks : String = ""
var advice : String = ""
}
Dao
#Dao
interface AdditiveDao {
#Query("SELECT * FROM Additive")
fun getAllAdditives() : LiveData<List<Additive>>
#Query("SELECT * FROM Additive WHERE id = :arg0")
fun getAdditiveById(id : String) : Additive
#Query("DELETE FROM Additive")
fun deleteAll()
#Insert(onConflict = REPLACE)
fun insert(additive: Additive)
#Update
fun update(additive: Additive)
#Delete
fun delete(additive: Additive)
}
ViewModel
class AdditiveViewModel(application: Application) : AndroidViewModel(application) {
private var elementDatabase : ElementDatabase
private val additivesModels : LiveData<List<Additive>>
init {
this.elementDatabase = ElementDatabase.get(appContext = getApplication())
this.additivesModels = this.elementDatabase.additivesModels().getAllAdditives()
}
fun getAdditivesList() : LiveData<List<Additive>> {
return this.additivesModels
}
fun deleteItem(additive : Additive) {
DeleteAsyncTask(this.elementDatabase).execute(additive)
}
private class DeleteAsyncTask internal constructor(private val db: ElementDatabase) : AsyncTask<Additive, Void, Void>() {
override fun doInBackground(vararg params: Additive): Void? {
db.additivesModels().delete(params[0])
return null
}
}
}
Fragment
class AdditivesFragment : LifecycleFragment() {
private var viewModel : AdditiveViewModel? = null
private var adapter : AdditivesAdapter? = null
companion object {
fun newInstance() : AdditivesFragment {
val f = AdditivesFragment()
val args = Bundle()
f.arguments = args
return f
}
}
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater?.inflate(R.layout.fragment_additives, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
this.adapter = AdditivesAdapter(ArrayList<Additive>())
this.additives_list.layoutManager = GridLayoutManager(this.context, 2, GridLayoutManager.VERTICAL, false)
this.additives_list.adapter = this.adapter
this.viewModel = ViewModelProviders.of(this).get(AdditiveViewModel::class.java)
this.viewModel?.getAdditivesList()?.observe(this, Observer<List<Additive>> { additivesList ->
if(additivesList != null) {
this.adapter?.addItems(additivesList)
}
})
super.onActivityCreated(savedInstanceState)
}
}
Now, my question is why is the observer called only once (at the start of the fragment) and then is not called back again? How can I keep the observer constantly listening to the changes in the DB (insert, update, delete) so that my recyclerView instantly can be updated? Thanks a lot for any suggestion.
This is where you made a mistake:
this.viewModel = ViewModelProviders.of(this).get(AdditiveViewModel::class.java)
you are passing this while you are inside the fragment which is pretty disturbing for some people cause it is not a syntax error but logical. You have to pass activity!! instead, it will be like this:
this.viewModel = ViewModelProviders.of(activity!!).get(AdditiveViewModel::class.java)
UPDATE:
Pass viewLifecycleOwner while being inside fragment while observing the Data
mainViewModel.data(viewLifecycleOwner, Observer{})
If you're using fragmentKtx, you can init viewModel this way:
private val viewModel by viewModels<MainViewModel>()
If You've viewModelFactory:
private val viewModel by viewModels<MainViewModel>{
viewModelFactory
}
with this approach you don't need to call:
// you can omit this statement completely
viewModel = ViewModelProviders.of(this).get(AdditiveViewModel::class.java)
You can simply just start observing the data..

Categories

Resources