RecyclerView not updating with list from XmlPullParse - android

So I'm attempting to write an app for playing podcasts from an RSS feed, mostly to challenge myself to see if I can pull it off, but I've ran into some trouble with populating the RecyclerViewer. I've been able to successfully parse the RSS feed and store it in a MutableList, using Log statements I can verify its working(in the background thread at least), but when I try to update the adapter nothing seems to happen.
I've been using The BigNerdRanch android book as my introduction to android, and I've looked at several examples for working with RecyclerViewers, but I cannot figure out what I'm doing wrong.
I can't help but wonder if I need to use a handler to pass the data from the background thread to the main thread. I can't remember where I read to use Executors.newSingleThreadExecutor() for executing the web call. I also don't know if I should be doing the actual parsing on the background thread along with the web call, or when and where I should be calling input.close() and connect.disconnect(). Or maybe I'm just inflating the wrong thing somewhere...
All the handler/adapter examples I look at are the same as what I have, the only real difference seems to be messing around threading.
At this point I only want to see it display the list.
Main Activity(I have a mockup splashScreenActivity class that starts MainActivity using runnable() and Handler().postDelayed())
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val currentFragment = supportFragmentManager.findFragmentById(R.id.fragment_container)
if (currentFragment == null){
val fragment = PodcastListFragment.newInstance()
supportFragmentManager.beginTransaction().add(R.id.fragment_container, fragment).commit()
}
}
I basically followed an example right out of the BigNerdRanch book, except they used a database and singleton repository to initially populate their RecyclerViewer. At this point I just want to display the list its generating before moving on to build a Database/Repository/ViewModel/etc.
class PodcastListFragment : Fragment() {
private var podcastList : MutableList<Podcast> = mutableListOf() //this is just for short term to see it work
private lateinit var podcastRecyclerView: RecyclerView
private var podcastAdapter: PodcastAdapter? = PodcastAdapter(podcastList)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.podcast_list, container, false)
podcastRecyclerView = view.findViewById(R.id.podcast_recycler_view) as RecyclerView
podcastRecyclerView.layoutManager = LinearLayoutManager(context)
podcastRecyclerView.adapter = podcastAdapter
updateUI(podcastList)
return view
}
override fun onAttach(context: Context) {
super.onAttach(context)
doInBackground() //not actually sure where this should be called
}
private fun doInBackground() {
val executor = Executors.newSingleThreadExecutor()
executor.execute {
try {
var podcast = Podcast()
val url = URL(RSS)
val connect: HttpURLConnection =
url.openConnection() as HttpURLConnection
connect.readTimeout = 10000
connect.connectTimeout = 15000
connect.requestMethod = "GET"
connect.connect()
val input: InputStream = connect.inputStream
val factory: XmlPullParserFactory = XmlPullParserFactory.newInstance()
factory.isNamespaceAware = true
val parser: XmlPullParser = factory.newPullParser()
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
parser.setInput(input, null)
var tagname: String?
var text = ""
var event = parser.eventType
while (event != XmlPullParser.END_DOCUMENT) {
tagname = parser.name
when (event) {
XmlPullParser.START_TAG -> if (tagname == "item") podcast = Podcast()
XmlPullParser.TEXT -> text = parser.text
XmlPullParser.END_TAG -> when(tagname){
"title" -> podcast.title = text
"itunes:author" -> podcast.author = text
"pubDate" -> podcast.date = text
"guid" -> podcast.id = parseGuid(text)
"itunes:summary" -> podcast.reference = text
"item" -> podcastList.add(podcast)
}
}
event = parser.next()
}
input.close()
connect.disconnect()
for (obj in podcastList) {Log.d(TAG, "guid: ${obj.id} :: Title: ${obj.title}")}
}
catch (e: Exception) { e.printStackTrace() }
catch (e: XmlPullParserException) { e.printStackTrace() }
catch (e: NullPointerException) { e.printStackTrace() }
}
}
// Log statements show the list is getting updated
private fun updateUI(podcasts: MutableList<Podcast>){
podcastAdapter = PodcastAdapter(podcasts)
podcastRecyclerView.adapter = podcastAdapter
}
private fun parseGuid(url: String) :String {
val equalsign = url.indexOf("=", 0, false)
return if ( equalsign != -1)
url.slice(IntRange(equalsign+1, url.length-1))
else ""
}
companion object{
fun newInstance(): PodcastListFragment{
return PodcastListFragment()
}
}
/**********************************************************************************************
*
* PodcastHolder
*
* *******************************************************************************************/
private inner class PodcastHolder(view: View) : RecyclerView.ViewHolder(view) {
private val podcastTitle: TextView = itemView.findViewById(R.id.podcast_title)
private val podcastDate: TextView = itemView.findViewById(R.id.podcast_date)
private val podcastScripture: TextView = itemView.findViewById(R.id.scripture_ref)
private val dateFormat = SimpleDateFormat("MMM d", Locale.getDefault()) //just use a string?
fun bind(podcast: Podcast) {
podcastTitle.text = podcast.title
podcastDate.text = podcast.date
podcastScripture.text = podcast.reference
}
}
/**********************************************************************************************
*
* PodcastAdapter
*
* *******************************************************************************************/
private inner class PodcastAdapter(var podcasts: MutableList<Podcast>) : RecyclerView.Adapter<PodcastHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PodcastHolder {
val view = layoutInflater.inflate(R.layout.podcast_list_item, parent, false)
return PodcastHolder(view)
}
override fun onBindViewHolder(holder: PodcastHolder, position: Int) {
val podcast = podcasts[position]
holder.bind(podcast)
}
override fun getItemCount(): Int = podcasts.size
}
I don't get any errors, just an empty RecyclerView, and a headache trying to figure out what I did wrong.
Any guidance would be greatly appreciated.
Thanks!
EDIT
After playing around with Thread.currentThread().name I was able to figure out it was a threading problem even though I wasn't seeing an exception thrown.

Call doInBackground() inside onCreateView()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.podcast_list, container, false)
podcastRecyclerView = view.findViewById(R.id.podcast_recycler_view) as RecyclerView
podcastRecyclerView.layoutManager = LinearLayoutManager(context)
podcastRecyclerView.adapter = podcastAdapter
doInBackground(); /// <<<<<<< change here
updateUI(podcastList)
return view
}
And add notifyDataSetChanged() after the background work is over in doInBackground()
private fun doInBackground() {
val executor = Executors.newSingleThreadExecutor()
executor.execute {
try {
/// .... omitted code
input.close()
connect.disconnect()
for (obj in podcastList) {Log.d(TAG, "guid: ${obj.id} :: Title: ${obj.title}")}
podcastAdapter.notifyDataSetChanged() // <<<< change here
/// ............ .... omitted code
}

Related

Trouble displaying errors for api response errors(kotlin)(android)

I am currently building a NewsApplication consisting of 7 different categories
App when working properly
The problem I am currently facing is, whenever I start the app, the app would send out 7 requests, however at times, some of the responses would result in the Sockettimeout error, which makes it awkward as some of the Fragments will be populated while the others will be blank.
I then tried a different method, I attempted to prevent any of the fragments from loading untill all of the responses are successful, however that will just leave me with a blank/Loading screen when one of the resonses suffer from a Sockettimeout error occurs.
**
Is there any way to force the app from displaying anything except for the error message when any of the responses suffer from an error?**
App when there is an error, like no internet connection or Sockettimeouterror
I am trying the find a way to block any fragments from loading when there is a Sockettimeout error and display the relevent Error Message.
Repository, I used the Callback interface to help me detect server side errors such as SocketTimeOutExeption
class NewsRepository(val db:RoomDatabases ) {
suspend fun upsert(article: Article) = db.getArticleDao().upsert(article)
fun getSavedNews() = db.getArticleDao().getAllArticles()
suspend fun deleteArticle(article: Article) = db.getArticleDao().deleteArticle(article)
suspend fun empty() = db.getArticleDao().isEmpty()
suspend fun nukeTable() = db.getArticleDao().nukeTable()
fun getNewsCall(country: String, Category: String?): MutableLiveData<MutableList<Article>> {
val call = RetrofitHelper.NewsApiCall.api.getNews(
country,
Category,
"5a3e054de1834138a2fbc4a75ee69053"
)
var Newlist = MutableLiveData<MutableList<Article>>()
call.enqueue(object : Callback<NewsDataFromJson> {
override fun onResponse(
call: Call<NewsDataFromJson>,
response: Response<NewsDataFromJson>
) {
if (response.isSuccessful) {
val body = response.body()
if (body != null) {
Newlist.value = body.articles
}
} else {
val jsonObj: JSONObject?
jsonObj = response.errorBody()?.string().let { JSONObject(it) }
if (jsonObj != null) {
MainActivity.apiRequestError = true
MainActivity.errorMessage = jsonObj.getString("message")
Newlist.value = mutableListOf<Article>()
}
}
}
override fun onFailure(call: Call<NewsDataFromJson>, t: Throwable) {
MainActivity.apiRequestError = true
MainActivity.errorMessage = t.localizedMessage as String
Log.d("err_msg", "msg" + t.localizedMessage)
}
})
return Newlist
}
}
MainActivity, this is where I call the requests
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
requestNews(GENERAL, generalNews,"us")
requestNews(TECHNOLOGY,TechNews,"us")
requestNews(HEALTH,healthNews,"us")
requestNews(SPORTS, SportsNews,"us")
requestNews(ENTERTAINMENT, EntertainmentNews,"us")
requestNews(SCIENCE, ScienceNews,"us")
requestNews(BUSINESS, BusinessNews,"us")
}
private fun requestNews(newsCategory: String, newsData: MutableList<Article>,country:String) {
viewModel.getNews(category = newsCategory, Country = country)?.observe(this) {
newsData.addAll(it)
totalRequestCount += 1
if(!apiRequestError){
if(totalRequestCount == 7){
ProgresBar.visibility = View.GONE
ProgresBar.visibility = View.GONE
setViewPager()
}
}else if(apiRequestError){
ProgresBar.visibility = View.GONE
FragmentContainer.visibility = View.GONE
val showError: TextView = findViewById(R.id.display_error)
showError.text = errorMessage
showError.visibility = View.VISIBLE
}
}
}
companion object{
var ScienceNews: MutableList<Article> = mutableListOf()
var EntertainmentNews: MutableList<Article> = mutableListOf()
var SportsNews: MutableList<Article> = mutableListOf()
var BusinessNews: MutableList<Article> = mutableListOf()
var healthNews: MutableList<Article> = mutableListOf()
var generalNews: MutableList<Article> = mutableListOf()
var TechNews: MutableList<Article> = mutableListOf()
var apiRequestError = false
var errorMessage = "error"
var SocketTimeout: JSONException? = null
}
}
ViewPagingFragment, this is where the ViewPager lives and this is where the FragmentAdapter is connected to.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val Categories = arrayListOf<String>("BreakingNews","Technology","Health","Science","Entertainment","Sports","Business")
viewpager(Categories)
viewPagerView = view.findViewById(R.id.view_pager)
viewPagerView.offscreenPageLimit = 7
var MainToolbarSaved = requireActivity().findViewById<Toolbar>(R.id.MenuToolBar)
var SecondaryToolBarSaved = requireActivity().findViewById<Toolbar>(R.id.topAppBarthesecond)
var MenuSavedButton = requireActivity().findViewById<ImageButton>(R.id.MenuSavedButton)
MainToolbarSaved.visibility = View.VISIBLE
SecondaryToolBarSaved.visibility = View.GONE
MenuSavedButton.setOnClickListener {
this.findNavController().navigate(R.id.action_global_savedFragment)
}
}
fun viewpager(FragmentList:ArrayList<String>){
val tabLayout = binding.tabLayout
PagerAdapter = FragmentAdapter(childFragmentManager,lifecycle)
binding.viewPager.adapter = PagerAdapter
tabLayout.tabMode = TabLayout.MODE_SCROLLABLE
TabLayoutMediator(tabLayout, binding.viewPager) { tab, position ->
tab.text = FragmentList[position]
}.attach()
}
Any tips on how I can do this?
I have attempted to look through other people's project and looked through the documentations for viewpager just to name a few.

moving the function from preferenceFragment to the second fragment

Well, I have a question, how to pass a function or its value to the second fragment? I am using the MVVM structure? I am exactly making an application in which in settingsFragment you select the csv file you want to read and send the result to the fragment with the graph and draws the graph for you. I've already done selecting the csv file but don't know how to read it and transfer the data from the file to the second fragment? Take a look at my code, if there is anything incomprehensible in the question or code, ask
SettingsFragment
class SettingsFragment : PreferenceFragmentCompat() {
private val SETTINGS_DEBUG = "PROFILE_DEBUG"
private var resolver = requireActivity().contentResolver
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
val myPref: Preference? = findPreference("load_csv_file") as Preference?
myPref?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "text/*"
startActivity(intent)
}catch (exc: Exception) {
Log.d(SETTINGS_DEBUG, exc.message.toString())
}
true
}
fun readCSV(uri: Uri?): List<String> {
if (uri != null) {
val csvFile = resolver.openInputStream(uri)
val isr = InputStreamReader(csvFile)
return BufferedReader(isr).readLines()
}
return Collections.emptyList()
}
}
}
ChartFragment
class ChartFragment : Fragment() {
private var _binding: FragmentChartBinding? = null
private val binding get() = _binding!!
private var resolver = requireActivity().contentResolver
private val lineChartVm by viewModels<ChartViewModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentChartBinding.inflate(inflater, container, false)
val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
val lines = readCSV(uri)
val newEntries = lines.map { line -> toEntry(line) }.groupingBy { entry -> entry.x }
.reduce { _, accumulator, element -> if (accumulator.y > element.y) accumulator else element }.values
val lineChart = binding.lineChart
val vl = LineDataSet(newEntries.toList().take(4000), "cost")
vl.setDrawValues(false)
vl.setDrawFilled(true)
vl.lineWidth = 1.5f
vl.fillColor = R.color.gray
vl.fillAlpha = R.color.red
vl.setDrawCircles(false)
lineChart.data = LineData(vl)
lineChart.notifyDataSetChanged()
lineChart.animateX(1800, Easing.EaseInExpo)
lineChart.description.isEnabled = false
lineChart.isHighlightPerDragEnabled = false
lineChart.isScaleYEnabled = false
lineChart.axisRight.isEnabled = false
}
val markerView = CustomMarker(activity?.applicationContext, R.layout.marker_view)
binding.lineChart.marker = markerView
return binding.root
}
private fun toEntry(line: String): Entry {
val split = line.split(";")
val time = split[1]
// idx 01234 012345 l:5 lub 6
// val 84504 165959
// 0, 3 - 845
val secondsStartIdx = time.length - 2
val minutesStartIdx = time.length - 4
val hoursStartIdx = (time.length - 6).coerceAtLeast(0)
val hour = time.substring(hoursStartIdx, hoursStartIdx + time.length - 4)
val minutest = time.substring(minutesStartIdx, minutesStartIdx + 2)
val seconds = time.substring(secondsStartIdx, secondsStartIdx + 2)
val newTime =
hour.toFloat() * 10000 + (minutest.toFloat() * 100 / 60).toInt() * 100 + (seconds.toFloat() * 100 / 60).toInt()
return Entry(newTime, split[2].toFloat())
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
There is not a lot of detail in the question, so I can only give a very general example and use made up class names and function arguments. This should give you an idea of the basic pattern to use.
Use a shared ViewModel in both Fragments. That means it is scoped to the Activity instead of the Fragment lifecycle. You can do this by using the activityViewModels() property delegate. In both Fragments, it would look like this:
val myViewModel: MyViewModel by activityViewModels()
In MVVM, your ViewModel class should be responsible for finding the file and reading it (or delegating to some other class to do so), so your readCSV function should be moved there. And it can parse the results into however you need them. File reading needs to be done on a background thread, and this is most easily done using coroutines started from viewModelScope. Results can be published to a LiveData.
class MyViewModel : ViewModel() {
private val mutableCsvResultLiveData = MutableLiveData<List<String>>()
val csvResultLiveData: LiveData<List<String>> get() = mutableCsvResultLiveData
fun readCSV(uri: Uri?, resolver: ContentResolver) {
uri ?: return // I would just make uri non-nullable so you don't need this
viewModelScope.launch(Dispatchers.IO) {
try {
val csvFileInputStream = resolver.openInputStream(uri)
check(csvFileInputStream != null) { "ContentResolver provider crashed" }
val isr = InputStreamReader(csvFileInputStream)
val result = BufferedReader(isr).readLines()
mutableCsvResultLiveData.postValue(result)
} catch (e: Exception) {
Log.e("readCSV", "Failed to read file", e)
return#launch
}
}
}
}
Then one fragment can initiate the file operation and parsing by calling myViewModel.readCSV(). The other fragment can observe the LiveData to react to the data when it arrives:
myViewModel.csvResultLiveData.observe(viewLifecycleOwner) { csvLines ->
// do something with csvLines, a List<String>
}

Input validation with MVVM and Data binding

I try to learn the MVVM Architecture by implementing a very simple app that takes three inputs from the user and stores them in a Room Database then display the data in a RecyclerView.
From the first try it seems to work well, then the app crashes if one of the inputs is left empty. Now, I want to add some input validations (for now the validations must just check for empty string), but I can't figure it out. I found many answers on stackoverflow and some libraries that validates the inputs, but I couldn't integrate those solutions in my app (most probably it is due to my poor implementation of the MVVM).
This is the code of my ViewModel:
class MetricPointViewModel(private val repo: MetricPointRepo): ViewModel(), Observable {
val points = repo.points
#Bindable
val inputDesignation = MutableLiveData<String>()
#Bindable
val inputX = MutableLiveData<String>()
#Bindable
val inputY = MutableLiveData<String>()
fun addPoint(){
val id = inputDesignation.value!!.trim()
val x = inputX.value!!.trim().toFloat()
val y = inputY.value!!.trim().toFloat()
insert(MetricPoint(id, x , y))
inputDesignation.value = null
inputX.value = null
inputY.value = null
}
private fun insert(point: MetricPoint) = viewModelScope.launch { repo.insert(point) }
fun update(point: MetricPoint) = viewModelScope.launch { repo.update(point) }
fun delete(point: MetricPoint) = viewModelScope.launch { repo.delete(point) }
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
}
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
}
}
and this is the fragment where everything happens:
class FragmentList : Fragment() {
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
//Binding object
private lateinit var binding: FragmentListBinding
//Reference to the ViewModel
private lateinit var metricPointVm: MetricPointViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
//Setting up the database
val metricPointDao = MetricPointDB.getInstance(container!!.context).metricCoordDao
val repo = MetricPointRepo(metricPointDao)
val factory = MetricPointViewModelFactory(repo)
metricPointVm = ViewModelProvider(this, factory).get(MetricPointViewModel::class.java)
// Inflate the layout for this fragment
binding = FragmentListBinding.inflate(inflater, container, false)
binding.apply {
lifecycleOwner = viewLifecycleOwner
myViewModel = metricPointVm
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initRecyclerview()
}
private fun displayPoints(){
metricPointVm.points.observe(viewLifecycleOwner, Observer {
binding.pointsRecyclerview.adapter = MyRecyclerViewAdapter(it) { selecteItem: MetricPoint -> listItemClicked(selecteItem) }
})
}
private fun initRecyclerview(){
binding.pointsRecyclerview.layoutManager = LinearLayoutManager(context)
displayPoints()
}
private fun listItemClicked(point: MetricPoint){
Toast.makeText(context, "Point: ${point._id}", Toast.LENGTH_SHORT).show()
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* #param param1 Parameter 1.
* #param param2 Parameter 2.
* #return A new instance of fragment FragmentList.
*/
// TODO: Rename and change types and number of parameters
#JvmStatic
fun newInstance(param1: String, param2: String) =
FragmentList().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
}
I'm planning also to add a long click to the recyclerview and display a context menu in order to delete items from the database. Any help would be appreciated.
My recycler view adapter implementation:
class MyRecyclerViewAdapter(private val pointsList: List<MetricPoint>,
private val clickListener: (MetricPoint) -> Unit): RecyclerView.Adapter<MyViewHolder>(){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding: RecyclerviewItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.recyclerview_item, parent, false)
return MyViewHolder(binding)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(pointsList[position], clickListener)
}
override fun getItemCount(): Int {
return pointsList.size
}
}
class MyViewHolder(private val binding: RecyclerviewItemBinding): RecyclerView.ViewHolder(binding.root){
fun bind(point: MetricPoint, clickListener: (MetricPoint) -> Unit){
binding.idTv.text = point._id
binding.xTv.text = point.x.toString()
binding.yTv.text = point.y.toString()
binding.listItemLayout.setOnClickListener{
clickListener(point)
}
}
}
Try the following,
fun addPoint(){
val id = inputDesignation.value!!.trim()
if(inputX.value == null)
return
val x = inputX.value!!.trim().toFloat()
if(inputY.value == null)
return
val y = inputY.value!!.trim().toFloat()
insert(MetricPoint(id, x , y))
inputDesignation.value = null
inputX.value = null
inputY.value = null
}
Edit:
you can try the following as well if you wish to let the user know that the value a value is expected
ViewModel
private val _isEmpty = MutableLiveData<Boolean>()
val isEmpty : LiveData<Boolean>
get() = _isEmpty
fun addPoint(){
val id = inputDesignation.value!!.trim()
if(inputX.value == null){
_isEmpty.value = true
return
}
val x = inputX.value!!.trim().toFloat()
if(inputY.value == null){
_isEmpty.value = true
return
}
val y = inputY.value!!.trim().toFloat()
insert(MetricPoint(id, x , y))
inputDesignation.value = null
inputX.value = null
inputY.value = null
}
//since showing a error message is an event and not a state, reset it once its done
fun resetError(){
_isEmpty.value = null
}
Fragment Class
metricPointVm.isEmpty.observe(viewLifecycleOwner){ isEmpty ->
isEmpty?.apply{
if(it){
// make a Toast
metricPointVm.resetError()
}
}
}

Handling screen rotation without losing data with viewModel - Android

I have one activity with unspecified orientation and there is one fragment attached to that activity that has different layouts for portrait and landscape mode and on that fragment, multiple API calls on a conditional basis, my problem is that when the screen rotates all data was lost and there is a lot of data on that fragment by which I don't want to save each data on saveInstance method. I tried android:configChanges="keyboardHidden|orientation|screenSize", but this didn't solve my problem. I want to handle this problem using viewModel. Please help, Thanks in advance.
Here is my code
Repository
class GetDataRepository {
val TAG = GetDataRepository::class.java.canonicalName
var job: CompletableJob = Job()
fun getData(
token: String?,
sslContext: SSLContext,
matchId: Int
): LiveData<ResponseModel> {
job = Job()
return object : LiveData<ResponseModel>() {
override fun onActive() {
super.onActive()
job.let { thejob ->
CoroutineScope(thejob).launch {
try {
val apiResponse = ApiService(sslContext).getData(
token
)
LogUtil.debugLog(TAG, "apiResponse ${apiResponse}")
withContext(Dispatchers.Main) {
value = apiResponse
}
} catch (e: Throwable) {
LogUtil.errorLog(TAG, "error: ${e.message}")
withContext(Dispatchers.Main) {
when (e) {
is HttpException -> {
value =
Gson().fromJson<ResponseModel>(
(e as HttpException).response()?.errorBody()
?.string(),
ResponseModel::class.java
)
}
else -> value = ResponseModel(error = e)
}
}
} finally {
thejob.complete()
}
}
}
}
}
}
fun cancelJob() {
job.cancel()
}
}
ViewMode:
class DataViewModel : ViewModel() {
val TAG = DataViewModel::class.java.canonicalName
var mListener: DataListener? = null
private val mGetDataRepository: GetDataRepository = GetDataRepository()
fun getData() {
LogUtil.debugLog(TAG, "getData")
if (mListener?.isInternetAvailable()!!) {
mListener?.onStartAPI()
val context = mListener?.getContext()
val token: String? = String.format(
context?.resources!!.getString(R.string.user_token),
PreferenceUtil.getUserData(context).token
)
val sslContext = mListener?.getSSlContext()
if (sslContext != null) {
val getData =
mGetDataRepository.getData(
token
)
LogUtil.debugLog(TAG, "getData ${getData}")
mListener?.onApiCall(getData)
} else {
LogUtil.debugLog(TAG, "getData Invalid certificate")
mListener?.onError("Invalid certificate")
}
} else {
LogUtil.debugLog(TAG, "getData No internet")
mListener?.onError("Please check your internet connectivity!!!")
}
LogUtil.debugLog(TAG, "Exit getData()")
}
}
Activity:
class DataActivity : AppCompatActivity() {
val TAG = DataActivity::class.java.canonicalName
lateinit var fragment: DataFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LogUtil.debugLog(TAG, "onCreate: Enter")
var binding: ActivityDataBinding =
DataBindingUtil.setContentView(this, R.layout.activity_data)
if (savedInstanceState == null) {
fragment = DataFragment.newInstance()
supportFragmentManager.beginTransaction().add(R.id.container, fragment, DataFragment.TAG)
} else {
fragment = supportFragmentManager.findFragmentByTag(DataFragment.TAG) as DataFragment
}
LogUtil.debugLog(TAG, "onCreate: Exit")
}
}
Fragment:
class DataFragment : Fragment(), DataListener {
private var mBinding: FragmentDataBinding? = null
private lateinit var mViewModel: DataViewModel
companion object {
val TAG = DataFragment::class.java.canonicalName
fun newInstance(): DataFragment {
return DataFragment()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mBinding =
DataBindingUtil.inflate(inflater, R.layout.fragment_data, container, false)
mViewModel = ViewModelProvider(this).get(DataViewModel::class.java)
mViewModel.mListener = this
getData()
return mBinding?.root
}
private fun getData() {
LogUtil.debugLog(TAG, "Enter getMatchScore()")
mViewModel.getData()
LogUtil.debugLog(TAG, "Exit getMatchScore()")
}
override fun <T> onApiCall(response: LiveData<T>) {
response.observe(this, Observer {
it as DataResponseModel
//
})
}
}
The lifecycle of viewModel by default is longer than your activity (in your case, screen rotation).
ViewModel will not be destroyed as soon as activity destroyed for configuration change, you can see this link.
You seem to have made a mistake elsewhere in your activity/fragment, please put your activity/fragment code here.
In your fragment you call mViewModel.getData() in your onCreateView, and every time you rotate your activity, this method call and all store data reset and fetched again!, simply you can check data of ViewModel in your fragment and if it's empty call getData(), it also seems your ViewModel reference to your view(Fragment) (you pass a listener from your fragment to your ViewModel) and it is also an anti-pattern (This article is recommended)

Android Room getById(id: Int) query returns a valid object when called from first fragment, but returns null when called from all the others

I am working on my school project, and I started getting this weird Room behavior. I have to admit, that everything used to work correctly, but after changing some things it stopped, and now it doesn't work even though I returned almost everything to where it was.
Here's my UserDao.kt:
#Dao
interface UserDao {
#Query("SELECT * FROM $USER_TABLE_NAME")
fun getAll(): LiveData<List<User>>
#Query("SELECT * FROM $USER_TABLE_NAME WHERE id = :id")
fun getById(id: Int): LiveData<User>
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(user: User)
#Update
fun update(user: User)
#Query("UPDATE $USER_TABLE_NAME SET pictureAddress = :image WHERE id = :id")
fun updateImageWhereId(id: Int, image: String)
#Delete
fun delete(user: User)
}
Here's the LoginFragment.kt, the first fragment, that gets loaded, when application starts. Here I check, if there is a user in the database and in case there is, I check, if passwords match. This is the place, where the query returns the user it supposed to be returning, and everything works.
class LoginFragment : Fragment() {
private lateinit var binding: FragmentLoginBinding
private lateinit var model: MainViewModel
private val navigation: INavigationCallback by lazy {
activity as INavigationCallback
}
data class IdPassword(var id: String = "", var password: String = "", var isCorrect: Boolean = true)
private val idPassword: IdPassword = IdPassword()
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? =
DataBindingUtil.inflate<FragmentLoginBinding>(inflater,
R.layout.fragment_login,
container, false).run {
binding = this
model = activity!!.let {
ViewModelProviders
.of(it, MainViewModel.Factory(it.application))
.get(MainViewModel::class.java)
}
lifecycleOwner = this#LoginFragment
idPassword = this#LoginFragment.idPassword
navigation = this#LoginFragment.navigation
applyLoginButton.setOnClickListener { tryLogin() }
return root
}
private fun tryLogin() {
//databaseData.getUserById - is basically a wrapper
model.databaseData.getUserById(idPassword.id.toInt()).observe(activity!!, Observer {
if(it != null && it.password == idPassword.password){
model.activeUserId = it.id //this stores active user's id
navigation.navigateTo(R.id.action_loginFragment_to_mainMenuFragment)
} else {
binding.errorLogin.visibility = View.VISIBLE
}
})
}
}
After logging in successfully, we get to the main menu screen, where I need once more to acquire user, to get his name and profile picture displayed.
Here's the MainMenuFragment.kt , the place, where the same query as above returns null. I also tried testing this, so read comments to understand better, what I have done:
class MainMenuFragment : Fragment() {
private lateinit var binding: FragmentMainMenuBinding
private val viewEffect: MainMenuViewUtils by lazy {
MainMenuViewUtils(binding.userNameSmall, binding.mainLinearLayoutTitle)
}
private val globalLayoutListener = object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
binding.root.viewTreeObserver.removeOnGlobalLayoutListener(this)
viewEffect.adjustCardSize(activity!!, binding.todayStatsCard)
}
}
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? = DataBindingUtil
.inflate<FragmentMainMenuBinding>(inflater,
R.layout.fragment_main_menu,
container, false).run {
binding = this
lifecycleOwner = this#MainMenuFragment
navigation = activity as INavigationCallback
viewModel = activity!!.let {
ViewModelProviders
.of(it, MainViewModel.Factory(it.application))
.get(MainViewModel::class.java)
}
viewModel?.let {
user = it.activeUser
it.setUpMainMenuObservers()
}
mainMenuAppbar.addOnOffsetChangedListener(
AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset ->
viewEffect.onOffsetChanged(appBarLayout, verticalOffset)
})
profilePicture.setOnClickListener {
Dialogs.profilePictureDialog(activity!!, viewModel!!).show()
}
mainMenuToolbar.setUpMainMenuToolbar(navigation!!)
root.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
viewEffect.startAlphaAnimation(userNameSmall, 0, View.INVISIBLE)
setHasOptionsMenu(true)
return root
}
private fun setUpBluetooth(): Boolean {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
activity?.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
return true
}
private fun MainViewModel.setUpMainMenuObservers(){
//this is the original call which doesn't work
activeUser.observe(activity!!, Observer {
log("Active user id = $activeUserId") //this prints correct id
if(it == null) log("USER IS NULL")
else log("USER : ${it.name}") // and this prints that user is null, so it wasn't found
tryCatch {
binding.profilePicture.setImageBitmap(getProfilePicture(it, 256))
}
})
//this was added for testing purposes. It returns a list of all users in the db.
databaseData.getAllUsers.observe(activity!!, Observer {
log("ALL USERS:")
it.forEach{user: User ->
log("NAME : ${user.name}, ID = ${user.id}") //this prints each user with the correct name and id
}
log("Active user id = $activeUserId") //this prints correct active user id
val user = it.first { id == activeUserId } // on this line app crashes, as if 1 != 1.
log("USER : ${user.name}")
tryCatch {
binding.profilePicture.setImageBitmap(getProfilePicture(user, 256))
}
})
bluetoothData.steps.observe(activity!!, Observer {
binding.stepsTodayCounter.text = it.toString()
})
bluetoothData.location.observe(activity!!, Observer {
val text = "${lastDayData.getLastDayDistanceFormatted()} Km"
binding.distanceTodayCounter.text = text
})
bluetoothData.isBluetoothConnected.observe(activity!!, Observer {
when (it) {
true -> binding.changeOnBTConnected("Connected!", R.drawable.bt_connected_icon)
false -> binding.changeOnBTConnected("Not Connected!", R.drawable.bt_disconnected_icon)
}
})
}
private fun FragmentMainMenuBinding.changeOnBTConnected(changeText: String, changeDrawable: Int){
connectionStatus.text = changeText
mainMenuToolbar.menu?.findItem(R.id.menu_bluetooth)?.icon =
activity!!.getDrawable(changeDrawable)
Toast.makeText(activity!!, "You are $changeText!", Toast.LENGTH_LONG).show()
}
private fun Toolbar.setUpMainMenuToolbar(navigation: INavigationCallback) {
inflateMenu(R.menu.main_view_menu)
menu.findItem(R.id.menu_bluetooth).setOnMenuItemClickListener {
setUpBluetooth()
}
menu.findItem(R.id.menu_settings).setOnMenuItemClickListener {
navigation.navigateTo(R.id.action_mainMenuFragment_to_settingsFragment)
true
}
}
}
So from the comments you can understand how weird it is. So in testing query, I would get prints in console like:
NAME : Slava Simonov, ID = 1
NAME : Uzi, ID = 11
Active user id = 1
Shutting down VM
FATAL EXCEPTION: main
...
I hope someone will be able to help me, because I have run out of ideas, why that could happen. It feels like some kind of bug. If you need some other pieces of my code, I can attach it, just feel free to ask.
Thanks to everybody in advance.

Categories

Resources