LocalDate.format causes OutofBount Exception in Observer - kotlin

I'm making an app in android using Kotlin, Material Design Components and the new architecture components.
I have an activity that starts a DialogFragment onCreate
The fragment has 6 Views that, via an observer, observe a different LiveDate for each and every one.
While checking all the this setup work I noticed that after 7 view switching I get
2020-05-12 20:43:19.346 4778-4778/package E/InputEventReceiver: Exception dispatching input event.
2020-05-12 20:43:19.346 4778-4778/package E/MessageQueue-JNI: Exception in MessageQueue callback: handleReceiveCallback
2020-05-12 20:43:19.357 4778-4778/package E/MessageQueue-JNI:
java.lang.ArrayIndexOutOfBoundsException: length=9; index=9
at android.text.Layout$HorizontalMeasurementProvider.get(Layout.java:1589)
...
I cheked the following things:
did all the setup on only one view -> still crashes
did all the setup on only one view but without using the "createDateFieldObserver" method -> still carshes
not calling the observer -> no crash
calling the observer but without the LocalDate.format -> no crash
I concluded the probleme is in the format function but I do not understand why.
The error is not pointing in that direction.
Any ideas?
Activity code
class UITestingActivity: FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_create_greenhouse)
val dialog = LabTimesDialogFragment()
dialog.show(supportFragmentManager, "LabTimes")
}
}
Fragment Code
class TimesDialogFragment : DialogFragment() {
companion object Companion {
private val TAG: String = "TimesDialog"
}
private val datesViewModel: TimesViewModel by activityViewModels()
private lateinit var datesViews: Map<LiveData<LocalDate>, TextInputEditText>
override fun onCreateDialog(savedInstanceState: Bundle?) : Dialog {
val viewsArray: Array<TextInputEditText>
return activity?.let {
val builder = AlertDialog.Builder(it)
val inflater = requireActivity().layoutInflater
val rootView: View = inflater.inflate(R.layout.dialog_filter_times, null)
builder.setView(rootView)
.setPositiveButton(R.string.feed) { dialog, id -> closeDialog() }
.setNegativeButton(R.string.cancel) { dialog, id -> getDialog()?.cancel() }
val dialog: AlertDialog = builder.create()
val fromSampling: TextInputEditText = rootView.findViewById(R.id.from_sampling) ?: throw IllegalStateException("Missing date view in LabTimesFilterDialog")
val toSampling: TextInputEditText = rootView.findViewById(R.id.to_sampling) ?: throw IllegalStateException("Missing date view in LabTimesFilterDialog")
val fromSending: TextInputEditText = rootView.findViewById(R.id.from_sending) ?: throw IllegalStateException("Missing date view in LabTimesFilterDialog")
val toSending: TextInputEditText = rootView.findViewById(R.id.to_sending) ?: throw IllegalStateException("Missing date view in LabTimesFilterDialog")
val fromReceiving: TextInputEditText = rootView.findViewById(R.id.from_receiving) ?: throw IllegalStateException("Missing date view in LabTimesFilterDialog")
val toReceiving: TextInputEditText = rootView.findViewById(R.id.to_receiving) ?: throw IllegalStateException("Missing date view in LabTimesFilterDialog")
datesViews = mapOf(datesViewModel.fromSampling to fromSampling,
datesViewModel.toSampling to toSampling,
datesViewModel.fromSending to fromSending,
datesViewModel.toSending to toSending,
datesViewModel.fromReceiving to fromReceiving,
datesViewModel.toReceiving to toReceiving
)
for ((liveData, textView) in datesViews) {
liveData.observe(this, createDateFieldObserver(textView))
textView.setOnClickListener { v ->
Log.d(TAG, "hello"+v.id)
}
}
return dialog
} ?: throw IllegalStateException("Activity cannot be null")
}
private fun closeDialog() {
// save dates to ViewModel
// closeDialog
TODO()
}
private fun createDateFieldObserver(tw: TextInputEditText): Observer<LocalDate> {
return Observer { date ->
Log.d(TAG, "obs"+tw.id)
tw.setText(date.format(DateTimeFormatter.ISO_DATE))
//tw.setText("hello")
}
}
}
ViewModel
class TimesViewModel : ViewModel() {
val fromSampling: MutableLiveData<LocalDate> = MutableLiveData(LocalDate.now())
val toSampling: MutableLiveData<LocalDate> = MutableLiveData(LocalDate.now())
val fromSending: MutableLiveData<LocalDate> = MutableLiveData(LocalDate.now())
val toSending: MutableLiveData<LocalDate> = MutableLiveData(LocalDate.now())
val fromReceiving: MutableLiveData<LocalDate> = MutableLiveData(LocalDate.now())
val toReceiving: MutableLiveData<LocalDate> = MutableLiveData(LocalDate.now())
}
it has been a while since I programmed for android. Everything I'm using here is new to me so if you spot an anti-pattern in this little code I would be glad to know.
Tnx

Turns out it was because of the layout.
There was no enough room in the view to display the whole date.
I don't understand why it causes indexoutofbound but the solution is to simply make the view "wrap_content" or bigger

Related

Kotlin Composable: How to return data from a called activity

In my Jetpack Compose project I have two activities: ActivityA and ActivityB. I can pass data from ActivityA to ActivityB easily, as follows:
private fun showContinueDialog(indexSent: Int, messageSent: String){
val intent = Intent(this, ActivityB::class.java)
intent.putExtra("indexSent", indexSent)
intent.putExtra("messageSent", messageSent)
startForResult.launch(intent)
}
and:
private val startForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
val intent = result.data
if (intent != null) {
//I expect to receive data here
}
}
}
In ActivityB I have:
lateinit var intent2: Intent
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
.......
intent2 = intent}
#Composable
fun ContinueClicked(indexReturn: Int) {
intent2.putExtra("indexSent", indexReturn)
setResult(Activity.RESULT_OK, intent2)
startActivity(intent2)
this.finish()
}
When ActivityB closes, I expect to receive the result (an Integer) to appear in the ActivityResult block of ActivityA, but it does not happen. How can I return data from ActivityB to the ActivityResult block of ActivityA? Thanks!
I figured it out:
In the second activity, add something like this where the activity terminates:
intent.putExtra("indexSent", 7788)
setResult(Activity.RESULT_OK, intent)
this.finish()

How to sort recyclerview data fetched from firebase by value in mvvm architecture

I am trying to sort datas which are fetched from firebase realtime database according to the value of a child using MVVM architecture the daabase reference is created in a repository
GroupNoticeRepository
class GroupNoticeRepository(private var groupSelected: String) {
val auth = Firebase.auth
val user = auth.currentUser!!.uid
private val scheduleReference: DatabaseReference =
FirebaseDatabase.getInstance().getReference("group-notice").child(groupSelected)
#Volatile
private var INSTANCE: GroupNoticeRepository? = null
fun getInstance(): GroupNoticeRepository {
return INSTANCE ?: synchronized(this) {
val instance = GroupNoticeRepository(groupSelected)
INSTANCE = instance
instance
}
}
fun loadSchedules(allSchedules: MutableLiveData<List<GroupNoticeData>>) {
scheduleReference.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
try {
val scheduleList: List<GroupNoticeData> =
snapshot.children.map { dataSnapshot ->
dataSnapshot.getValue(GroupNoticeData::class.java)!!
}
allSchedules.postValue(scheduleList)
} catch (_: Exception) {
}
}
override fun onCancelled(error: DatabaseError) {
}
})
}
}
GroupNoticeFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recycler = binding.taskList
recycler.layoutManager = LinearLayoutManager(context)
recycler.setHasFixedSize(true)
adapter = GroupNoticeAdapter(_inflater)
recycler.adapter = adapter
viewModel = ViewModelProvider(this)[GroupNoticeViewModel::class.java]
viewModel.initialize(groupId)
viewModel.allSchedules.observe(viewLifecycleOwner) {
adapter!!.updateUserList(it)
}
}
GroupNoticeViewModel
class GroupNoticeViewModel : ViewModel() {
private lateinit var repository: GroupNoticeRepository
private val _allSchedules = MutableLiveData<List<GroupNoticeData>>()
val allSchedules: LiveData<List<GroupNoticeData>> = _allSchedules
fun initialize(groupSelected: String) {
repository = GroupNoticeRepository(groupSelected).getInstance()
repository.loadSchedules(_allSchedules)
}
}
`
As you can see the current structure
group-notice
-groupId(groups)
-noticeId (notices)
- taskDate
Here under group notice there are some groups and in each group there are some notices(noticeId) .
Each notice has a task date . Now I am trying to sort the notices according to the taskdate meaning the taskDate which will is closer to todays date will view first in the recycler view. Or the notice with latest taskdate given will appear first in the recycler view .
Just as hassan bazai said I followed the same concept of comparing two dates
class FirebaseDataComparator : Comparator<GroupNoticeData?> {
override fun compare(p0: GroupNoticeData?, p1: GroupNoticeData?): Int {
val dateFormat = SimpleDateFormat("dd/MM/yyyy")
val firstDate: Date = dateFormat.parse(p0?.taskdate!!) as Date
val secondDate: Date = dateFormat.parse(p1?.taskdate!!) as Date
return firstDate.compareTo(secondDate)
}
}
Here groupNoticeData is the data class I am using to populate the data in my recycler View and took two objects of them . Parsed their date format accordingly and later on compared them.
And in the recyclerViewAdapter before adding my data, I sorted them using the comparator class and added them later on. Here is the part where I had to use the comparator class.
fun updateNoticeList(notices: List<GroupNoticeData>) {
Collections.sort(notices, FirebaseDataComparator())
this.tasks.clear()
this.tasks.addAll(notices)
notifyDataSetChanged()
}

Can ViewModel observe its MutableLiveData property?

I'm trying to use ViewModels with LiveData and MutableLiveData in Android for the first time. I have not read all documentation yet.
My question:
How can I call fun applyFilters() when val filterByName = MutableLiveData<String>() was changed from outside (from a Fragment).
In .NET MVVM I would just add that call in the setter for filterByName property. But how to I do it in with MutableLiveData?
I think I should use MediatorLiveData, but this documentation does not explain much.
I was thinking about using the observer inside a viewmodel (so it could observe itself), but then I would have to use AndroidViewModel instead of ViewModel, to get lifecycleOwner which is required to observe MutableLiveData...
My code:
In my AssignUhfTagToInventoryItemViewModel() : ViewModel() (which is shared between few fragments) I have properties like this:
/* This is filled only once in SomeViewModel.init() function, always contains all data */
var _items = ArrayList<InventoryItemDto?>()
/* This is filled from _items, contains filtered data */
val items = MutableLiveData<ArrayList<InventoryItemDto?>>().apply { value = ArrayList() }
val filterByName = MutableLiveData<String>().apply { value = "" }
And a function like this:
fun applyFilters(){
Log.d(TAG, "ViewModel apply filters called. Current name filter: ${filterByName.value}")
items.value?.clear()
val nameFilterLowercase = filterByName.value.toString().lowercase()
if (!filterByName.value.isNullOrEmpty()) {
for (item in _items) {
val itemNameLowercase = item?.name?.lowercase()
if (itemNameLowercase?.contains(nameFilterLowercase) == true)
items.value?.add(item)
}
Log.d(TAG, "Filter is on, data cached : ${_items.count()} rows")
Log.d(TAG, " data displayed: ${items.value?.count()} rows")
}
else {
items.value?.addAll(_items)
Log.d(TAG, "Filter is off, data cached : ${_items.count()} rows")
Log.d(TAG, " data displayed: ${items.value?.count()} rows")
}
}
And this is part of my Fragment where I use that viewmodel:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentInventoryItemSelectBinding.bind(view)
val vm = ViewModelProvider(requireActivity())[AssignUhfTagToInventoryItemViewModel::class.java]
// old school ArrayAdapter
val listAdapter = InventoryItemViewAdapter(this.context, vm.items.value)
binding.lvItems.adapter = listAdapter // old school ListView
binding.tvFilterByName.addTextChangedListener(AfterTextChangedWatcher {
vm.filterByName.postValue(it)
/*
IN MY FRAGMENT I WANT TO AVOID FUNCTION CALL BELOW
*/
vm.applyFilters()
listAdapter.notifyDataSetInvalidated()
Log.d(TAG, "afterTextChanged: $it")
})
vm.items.observe(requireActivity()){
listAdapter.notifyDataSetInvalidated()
}
}
Technically? I guess you can but you can use Transformations.switchMap to take input from one LiveData and apply them to the source from another.
var _items = MutableLiveData(ArrayList<InventoryItemDto?>())
val filterByName = MutableLiveData<String>().apply { value = "" }
val items = Transformations.switchMap(filterByName) { filter ->
val nameFilterLowercase = filter.lowercase()
_items.map { mappedItems ->
mappedItems.filter { listItem ->
listItem.contains(nameFilterLowercase)
}
}
}
vm.items.observe(viewLifecycleOwner) { items ->
// old school ArrayAdapter
val listAdapter = InventoryItemViewAdapter(this.context, items)
binding.lvItems.adapter = listAdapter // old school ListView
}

Change API param based on Dialog Fragment input with MVVM in Kotlin

i'm a beginner in android & kotlin and i'm having an issue i been trying to figure out all day...
I have an app that fetches data from NewsApi and displays it in a recycler view , i am using Retrofit library and Room (to save favorite articles) with MVVM architecture. I want to add an option so that the user can select the country of the news from a dialog that pops up by clicking on a icon on the toolbar menu.
I have created a custom DialogFragment and have it show up, the dialog contains a spinner with a list of countries and i'm using FragmentResult and FragmentResultListener to pass the country value between dialog fragment and news fragment.
DialogFragment
class CountrySelectDialog : DialogFragment(R.layout.country_selection_dialog) {
private lateinit var binding: CountrySelectionDialogBinding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = CountrySelectionDialogBinding.bind(view)
binding.spCountrySelection.onItemSelectedListener =
object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
adapterView: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
Toast.makeText(
context,
"you selected ${adapterView?.getItemAtPosition(position).toString()}",
Toast.LENGTH_SHORT
).show()
}
override fun onNothingSelected(adapterView: AdapterView<*>?) {
}
}
binding.btnCancel.setOnClickListener {
this.dismiss()
}
binding.btnConfirm.setOnClickListener {
val result = binding.spCountrySelection.selectedItem.toString()
setFragmentResult("countryCode", bundleOf("bundleKey" to result))
this.dismiss()
}
}
}
The news Fragment is observing data from the View Model
class BreakingNewsFragment : Fragment(R.layout.fragment_breaking_news) {
lateinit var viewModel: NewsViewModel
lateinit var newsAdapter: NewsAdapter
private lateinit var binding: FragmentBreakingNewsBinding
val TAG = "BreakingNewsFragment"
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentBreakingNewsBinding.bind(view)
viewModel = (activity as NewsActivity).viewModel
setUpRecyclerView()
setFragmentResultListener("countryCode") { countryCode, bundle ->
val result = bundle.getString("countryCode")
viewModel.countryCode = result!!}
viewModel.breakingNews.observe(viewLifecycleOwner, Observer {
when (it) {
is Resource.Success -> {
hideProgressBar()
it.data?.let {
newsAdapter.differ.submitList(it.articles.toList())
val totalPages = it.totalResults / QUERY_PAGE_SIZE + 2
isLastPage = viewModel.breakingNewsPage == totalPages
}
}
is Resource.Error -> {
hideProgressBar()
it.message?.let {
Log.e(TAG, "An error occurred: $it")
}
}
is Resource.Loading -> {
showProgressBar()
}
}
})
newsAdapter.setOnItemClickListener {
val bundle = Bundle().apply {
putSerializable("article", it)
}
findNavController().navigate(
R.id.action_breakingNewsFragment_to_articleFragment, bundle
)
}
}
ViewModel:
class NewsViewModel(val newsRepository: NewsRepository, val app: Application) : AndroidViewModel(app) {
val breakingNews: MutableLiveData<Resource<NewsResponse>> = MutableLiveData()
var breakingNewsPage = 1
var breakingNewsResponse: NewsResponse? = null
val searchNews: MutableLiveData<Resource<NewsResponse>> = MutableLiveData()
var searchNewsPage = 1
var searchNewsResponse: NewsResponse? = null
var countryCode :String = "it"
init {
getBreakingNews(countryCode)
}
fun getBreakingNews(countryCode: String) {
viewModelScope.launch {
breakingNews.postValue(Resource.Loading())
val response = newsRepository.getBreakingNews(countryCode, breakingNewsPage)
breakingNews.postValue(handleBreakingNewsResponse(response))
}
}
fun handleBreakingNewsResponse(response: Response<NewsResponse>): Resource<NewsResponse> {
if (response.isSuccessful) {
response.body()?.let { resultResponse ->
breakingNewsPage++
if (breakingNewsResponse == null) {
breakingNewsResponse = resultResponse
} else {
val oldArticles = breakingNewsResponse?.articles
val newArticles = resultResponse.articles
oldArticles?.addAll(newArticles)
}
return Resource.Success(breakingNewsResponse ?: resultResponse)
}
}
return Resource.Error(response.message())
}
fun searchNews(searchQuery: String) {
viewModelScope.launch {
searchNews.postValue(Resource.Loading())
val response = newsRepository.searchNews(searchQuery, searchNewsPage)
searchNews.postValue((handleSearchNewsResponse(response)))
}
}
fun handleSearchNewsResponse(response: Response<NewsResponse>): Resource<NewsResponse> {
if (response.isSuccessful) {
response.body()?.let { resultResponse ->
searchNewsPage++
if (searchNewsResponse == null) {
searchNewsResponse = resultResponse
} else {
val oldArticles = searchNewsResponse?.articles
val newArticles = resultResponse.articles
oldArticles?.addAll(newArticles)
}
return Resource.Success(searchNewsResponse ?: resultResponse)
}
}
return Resource.Error(response.message())
}
}
When i click on the icon on the toolbar menu the dialog appears and works fine but i can't seem to find a way to have the recycler view update with new data using given value for country
I searched everywhere and couldn't find a solution (or probably didn't understand it :S) can someone guide me into the right direction? I'm so lost...
When I click on the icon on the toolbar menu the dialog appears and works fine but I can't seem to find a way to have the recycler view update with new data using given value for country.

LiveData always returns LiveData<Object>?

I'm trying to put a recyclerview which get its data from room. My function getAllHomework returns LiveData<List<Homework>>, but when I tried to set the return value to the recyclerview adapter, it will always return this error
Type Mismatch.
Required: List<Homework>
Found: List<Homework>?
Here's my HomeworkViewModel class which has the function getAllHomework looks like:
class HomeworkViewModel : ViewModel() {
private var matrixNumber: String? = null
private var schoolID: Int = 0
lateinit var listAllHomework: LiveData<List<Homework>>
lateinit var homeworkRepository: HomeworkRepository
fun init(params: Map<String, String>) {
schoolID = Integer.parseInt(params["schoolID"])
homeworkRepository = HomeworkRepository()
listAllHomework = homeworkRepository.getAllHomework(1, "2018")
}
fun getAllHomework(): LiveData<List<Homework>>{
return listAllHomework
}
}
And below is the part in my Homework activity that tries to set the value into recyclerview adapter but will always return the type mismatch error.
class Homework : AppCompatActivity(), LifecycleOwner {
lateinit var linearLayoutManager: LinearLayoutManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.homework)
linearLayoutManager = LinearLayoutManager(this)
rvHomeworks.layoutManager = linearLayoutManager
var adapter = AdapterHomework(this)
rvHomeworks.adapter = adapter
var homeworkViewModel = ViewModelProviders.of(this).get(HomeworkViewModel::class.java)
homeworkViewModel.init(params)
homeworkViewModel.getAllHomework().observe(this, Observer {
allHomework -> adapter.setHomeworkList(allHomework)
})
}
}
The line allHomework -> adapter.setHomeworkList(allHomework) above will show the Type Mismatch error I mentioned above.
Here's how my AdapterHomework looks like:
class AdapterHomework(context: Context): RecyclerView.Adapter<AdapterHomework.ViewHolder>() {
lateinit var homeworkList: List<Homework>
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder{
val v = LayoutInflater.from(parent.context).inflate(R.layout.rv_homeworks, parent, false)
return ViewHolder(v);
}
#JvmName("functionToSetTheHomeworkList")
fun setHomeworkList(myHomeworkList: List<Homework>){
homeworkList = myHomeworkList
notifyDataSetChanged()
}
}
I could not find where in my code did I ever return List<Homework>? instead of List<Homework>.
This has actually nothing to do with Room, but with how LiveData was designed - specifically Observer class:
public interface Observer<T> {
void onChanged(#Nullable T t);
}
as you can see T (in your case List<Homework>) is marked as #Nullable therefore you will always get it's Kotlin equivalent List<Homework>? as a parameter of onChanged in your Observer implementation.
I would recommend changing setHomeworkList to something like this:
fun setHomeworkList(myHomeworkList: List<Homework>?){
if(myHomeworkList != null){
homeworkList = myHomeworkList
notifyDataSetChanged()
}
}
You can also use let function for that like this:
fun setHomeworkList(myHomeworkList: List<Homework>?){
myHomeworkList?.let {
homeworkList = it
notifyDataSetChanged()
}
}