I know this error is very common, but I couldn't find a solution to my problem in the way too many places this error is brought up.
I'm developing an app in order to store and sort TV shows. I've got a main activity with some fragments, with a HomeFragment which is the home page, with an 'Add show' button, and below a recyclerView with all my shows.
When clicking on the 'Add show' button, I start a new activity in order to fill a form and then create the show with the provided informations. No problem here, that works as it should. Now I'm trying to add the possibility to edit the shows by clicking on them in the recyclerView I talked about above. This also brings up the same activity as the 'Add show' button, but this time with the show informations.
And this is from this page that the problem seems to be coming. In the form activity, I have an button in which I pick an image for the show. When editing the show, if I change the image, I get no error, but if I change something else, for example the name, without changing this image, when clicking the confirm button, the show is correctly edited but the app crashes with the java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState error.
The error seems to be coming from the fragment part, with the transaction being unable to commit (I've searched for a while so I began to understand why that wasn't working, but couldn't determine which part of the code makes it this way). Here is the fragment:
class HomeFragment(private val context: MainActivity) : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_home, container, false)
view.findViewById<Button>(R.id.button_add_show).setOnClickListener{
startActivity(Intent(context, AddShowActivity::class.java))
}
val verticalRecyclerView = view.findViewById<RecyclerView>(R.id.vertical_recycler_view)
verticalRecyclerView.adapter = ShowAdapter(context, showList, R.layout.item_show)
return view
}
}
And here the MainActivity part where it's loaded:
private fun loadFragment(fragment: Fragment){
// Load repository
val repo = ShowRepository()
// Update shows list
repo.updateData{
// Inject fragment into fragment_container
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, fragment)
transaction.addToBackStack(null)
transaction.commit()
}
}
Here is the code of my AddShowActivity, which renders the form to fill:
class AddShowActivity : AppCompatActivity() {
private var fileImage: Uri? = null
private lateinit var uploadedImage: ImageView
private lateinit var editTextName: EditText
private lateinit var editTextNote: EditText
private lateinit var editTextDescription: EditText
private lateinit var editTextReview: EditText
private lateinit var datePicker: DatePicker
private var currentShow: ShowModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_show)
setupComponents()
setupButtons()
// Get show when editing
if(intent.extras != null){
val position = intent.extras!!.getInt("position")
currentShow = showList[position]
}
initializeComponents()
}
private fun setupComponents() {
editTextName = findViewById(R.id.name_input)
editTextNote = findViewById(R.id.note_input)
editTextDescription = findViewById(R.id.description_input)
editTextReview = findViewById(R.id.review_input)
uploadedImage = findViewById(R.id.preview_image)
datePicker = findViewById(R.id.watch_date_input)
}
private fun setupButtons(){
val pickupImageButton = findViewById<Button>(R.id.upload_image_button)
pickupImageButton.setOnClickListener{
pickupImage()
}
val confirmButton = findViewById<Button>(R.id.confirm_button)
confirmButton.setOnClickListener{
sendForm()
val toastText = when(currentShow){
null -> "Show added"
else -> "Show edited"
}
Toast.makeText(this, toastText, Toast.LENGTH_SHORT).show()
this.finish()
}
}
#SuppressLint("NewApi")
private fun initializeComponents() {
if(currentShow != null){
editTextName.setText(currentShow!!.name)
editTextNote.setText(currentShow!!.note.toString())
editTextDescription.setText(currentShow!!.description)
editTextReview.setText(currentShow!!.review)
Glide.with(this).load(Uri.parse(currentShow!!.imageUrl)).into(uploadedImage)
}
}
private fun sendForm(){
val repo = ShowRepository()
if(fileImage == null)createShow(repo)
else{
if(currentShow != null)repo.deleteImage(currentShow!!)
repo.uploadImage(fileImage!!){
createShow(repo)
}
}
}
private fun createShow(repo: ShowRepository){
val showName = editTextName.text.toString()
val showNote = parseInt(editTextNote.text.toString())
val description = editTextDescription.text.toString()
val review = editTextReview.text.toString()
val showWatchDate = getWatchDate(datePicker)
val downloadImageUrl = downloadImageUri.toString()
val show = ShowModel(UUID.randomUUID().toString(), showName, showWatchDate, showNote, downloadImageUrl, description, review)
if(currentShow != null){
show.id = currentShow!!.id
repo.updateShow(show)
}
else repo.insertShow(show)
}
private fun getWatchDate(datePicker: DatePicker): String {
var day = datePicker.dayOfMonth.toString()
if(day.toInt() < 10)day = "0$day"
var month = (datePicker.month + 1).toString()
if(month.toInt() < 10)month = "0$month"
val year = datePicker.year.toString()
return "$day-$month-$year"
}
private fun pickupImage(){
val intent = Intent()
intent.type = "image/"
intent.action = Intent.ACTION_GET_CONTENT
startActivityForResult(Intent.createChooser(intent, "Select Picture"), 47)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(resultCode == Activity.RESULT_OK && requestCode == 47){
if(data == null || data.data == null)return
fileImage = data.data
uploadedImage.setImageURI(fileImage)
}
}
}
Here the ShowRepository, which handles the communication with the Firebase database:
class ShowRepository {
object Singleton{
// Link to bucket
private val BUCKET_URL: String = "gs://tv-memories.appspot.com"
// Storage connexion
val storageReference = FirebaseStorage.getInstance().getReferenceFromUrl(BUCKET_URL)
// Database connexion
val databaseRef = FirebaseDatabase.getInstance().getReference("shows")
// List containing all shows
val showList = arrayListOf<ShowModel>()
// Contains current image link
var downloadImageUri: Uri? = null
}
fun updateData(callback: () -> Unit){
// Absorb data from databaseRef
databaseRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
// Remove old shows
showList.clear()
// Get list
for(ds in snapshot.children){
//Build show object
val show = ds.getValue(ShowModel::class.java)
// Verify show isn't null
if(show != null){
// Add show to the list
showList.add(show)
}
}
// Activate callback
callback()
}
override fun onCancelled(p0: DatabaseError) {
}
})
}
// Upload files on storage
fun uploadImage(file: Uri, callback: () -> Unit){
val fileName = UUID.randomUUID().toString() + ".jpg"
val ref = storageReference.child(fileName)
val uploadTask = ref.putFile(file)
uploadTask.continueWithTask(Continuation<UploadTask.TaskSnapshot, Task<Uri>>{ task ->
if(!task.isSuccessful){
task.exception?.let{throw it}
}
return#Continuation ref.downloadUrl
}).addOnCompleteListener{ task ->
if(task.isSuccessful){
downloadImageUri = task.result
callback()
}
}
}
fun deleteImage(show: ShowModel){
val photoRef: StorageReference = FirebaseStorage.getInstance().getReferenceFromUrl(show.imageUrl)
photoRef.delete()
}
fun updateShow(show: ShowModel) = databaseRef.child(show.id).setValue(show)
fun insertShow(show: ShowModel) = databaseRef.child(show.id).setValue(show)
fun deleteShow(show: ShowModel){
databaseRef.child(show.id).removeValue()
deleteImage(show)
}
}
And the full traceback of the error:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: fr.steph.showmemories, PID: 18296
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at androidx.fragment.app.FragmentManager.checkStateLoss(FragmentManager.java:1844)
at androidx.fragment.app.FragmentManager.enqueueAction(FragmentManager.java:1884)
at androidx.fragment.app.BackStackRecord.commitInternal(BackStackRecord.java:329)
at androidx.fragment.app.BackStackRecord.commit(BackStackRecord.java:294)
at fr.steph.showmemories.MainActivity$loadFragment$1.invoke(MainActivity.kt:49)
at fr.steph.showmemories.MainActivity$loadFragment$1.invoke(MainActivity.kt:44)
at fr.steph.showmemories.ShowRepository$updateData$1.onDataChange(ShowRepository.kt:61)
at com.google.firebase.database.core.ValueEventRegistration.fireEvent(ValueEventRegistration.java:75)
at com.google.firebase.database.core.view.DataEvent.fire(DataEvent.java:63)
at com.google.firebase.database.core.view.EventRaiser$1.run(EventRaiser.java:55)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7078)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:494)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:964)
I finally found a solution to my problem, which is to replace transaction.commit() by transaction.commitAllowingStateLoss() in my loadFragment() method in case the state has been saved.
I then get
private fun loadFragment(fragment: Fragment){
// Load repository
val repo = ShowRepository()
// Update shows list
repo.updateData{
// Inject fragment into fragment_container
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, fragment)
transaction.addToBackStack(null)
if(supportFragmentManager.isStateSaved)transaction.commitAllowingStateLoss()
else transaction.commit()
}
}
Related
I have created a meditation application that users can select the class they want from the recycler view within the fragment, then it will show the detailed content and steps in a new activity. In the new activity, I have implemented a audio playing functions where user can stream the audio file from the firebase. The problem now is whenever I switch back to the recycler view within the fragment, the audio file still plays in the background. How do I stop the audio file from playing whenever I switch to another page within the app or switch to another app ?
This is the code for the recycler view within the fragment
class ClassFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
private lateinit var classArrayList: ArrayList<Classes>
private lateinit var tempArraylist: ArrayList<Classes>
private lateinit var classAdapter: ClassAdapter
private lateinit var db: FirebaseFirestore
var mediaPlayer: MediaPlayer? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view: View = inflater.inflate(R.layout.fragment_class, container, false)
recyclerView = view.findViewById(R.id.ClassList)
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.setHasFixedSize(true)
classArrayList = arrayListOf()
tempArraylist = arrayListOf()
eventChangeListener()
classAdapter = ClassAdapter(requireContext(), classArrayList)
recyclerView.adapter = classAdapter
return view
}
private fun eventChangeListener() {
db = FirebaseFirestore.getInstance()
db.collection("class").addSnapshotListener(object : EventListener<QuerySnapshot> {
override fun onEvent(value: QuerySnapshot?, error: FirebaseFirestoreException?) {
if (error != null) {
Log.e("Firestore error", error.message.toString())
return
}
for (dc: DocumentChange in value?.documentChanges!!) {
if (dc.type == DocumentChange.Type.ADDED) {
classArrayList.add(
dc.document.toObject(
(Classes::class.java)
)
)
}
}
tempArraylist.clear()
tempArraylist.addAll(classArrayList)
classAdapter.notifyDataSetChanged()
}
})
}
}
This is the code for the new activity page where it will show content when the user selects from recyclerview
class DetailActivity : AppCompatActivity() {
private lateinit var imageViewClass: ImageView
private lateinit var textViewClassName: TextView
private lateinit var textViewClassDes: TextView
private lateinit var textViewClassContent: TextView
private var stop:Boolean = false
var mediaPlayer: MediaPlayer? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_detail)
val btnPlay: Button = findViewById(R.id.btnPlay)
val btnStop: Button = findViewById(R.id.btnStop)
imageViewClass = findViewById(R.id.imageViewClassImage)
textViewClassName = findViewById(R.id.textViewClassName)
textViewClassDes = findViewById(R.id.textViewClassDescription)
textViewClassContent = findViewById(R.id.textViewClassContent)
val classImage = intent.getStringExtra("class_image")
val className = intent.getStringExtra("class_name")
val classDes = intent.getStringExtra("class_des")
val classContent = intent.getStringExtra("class_content")
val classAudio = intent.getStringExtra("class_audio")
Glide.with(this)
.load(classImage)
.centerCrop()
.into(imageViewClass)
textViewClassName.text = className
textViewClassDes.text = classDes
textViewClassDes.text = classDes!!.replace("\\n", "\n")
textViewClassContent.text = classContent
textViewClassContent.text = classContent!!.replace("\\n", "\n")
btnPlay.isEnabled = true
btnStop.isEnabled = false
btnPlay.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
if (mediaPlayer == null) {
mediaPlayer = MediaPlayer.create(this#DetailActivity, Uri.parse(classAudio))
mediaPlayer!!.isLooping = true
mediaPlayer!!.start()
btnStop.isEnabled = true
btnPlay.isEnabled = false
Toast.makeText(applicationContext,"Audio Starts",Toast.LENGTH_SHORT).show()
} else mediaPlayer!!.start()
}
})
mediaPlayer?.setOnCompletionListener {
btnPlay.isEnabled = true
btnStop.isEnabled = false
Toast.makeText(this,"end",Toast.LENGTH_SHORT).show()
}
btnStop.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
if(mediaPlayer!!.isPlaying){
stop = false
mediaPlayer!!.stop()
mediaPlayer!!.reset()
mediaPlayer!!.release()
mediaPlayer = null
btnPlay.isEnabled = true
btnStop.isEnabled = false
Toast.makeText(applicationContext,"Audio Stops",Toast.LENGTH_SHORT).show()
}
}
})
}
}
This is a classic case of onDestroy of the Activity being called. I'll keep it short but there is a lifecycle through which an Activity undergoes and when you seem to come back from the SecondActivity to the first Activity, the onDestroy of your second activity is called to perform the destruction function
So the solution, override the onDestroy method in your DetailActivity class and do something like this
override fun onDestroy(){
if(mediaPlayer != null){
mediaPlayer!!.stop()
mediaPlayer!!.reset()
mediaPlayer!!.release()
mediaPlayer = null
}
}
You can read in detail about Activity Lifecyle
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>
}
I apologize if my title was a bit general, but I currently have a journal app where the user can pick an image when making a new entry and insert it into a database, which gets shown in a recyclerview later.
class addFragment : Fragment(), EasyPermissions.PermissionCallbacks, EasyPermissions.RationaleCallbacks {
lateinit var bitmap: Bitmap
private val mJournalViewModel: JournalViewModel by viewModels()
private val mSharedViewModel: SharedViewModel by viewModels()
var currentDate: String? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_add, container, false)
// set menu
setHasOptionsMenu(true)
view.mood_spinner.onItemSelectedListener = mSharedViewModel.listener
// Set date and time for currentDate
val sdf = SimpleDateFormat("dd/M/yyyy hh:mm a")
currentDate = sdf.format(Date())
// Opens gallery when image button clicked, gets image
view.image_et.setOnClickListener {
readStorageTask()
//Intent to pick image
val intent = Intent(Intent.ACTION_PICK)
intent.type = "image/*"
startActivityForResult(intent, 1001)
}
return view
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.clear()
}
// Handle result of picked image
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK && requestCode == 1001) {
// Converts image URI to bitmap
if (data != null && data.data != null) {
val uri = data.data!!
val inputStream = requireContext().contentResolver.openInputStream(uri)
val cursor = requireContext().contentResolver.query(uri, null, null, null, null)
cursor?.use { c ->
val nameIndex = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (c.moveToFirst()) {
val name = c.getString(nameIndex)
inputStream?.let { inputStream ->
// create same file with same name
val file = File(requireContext().cacheDir, name)
val os = file.outputStream()
os.use {
inputStream.copyTo(it)
}
val bitmapUnprocessed = BitmapFactory.decodeFile(file.absolutePath)
bitmap = resizedBitmap(bitmapUnprocessed)
preview_image.setImageBitmap(bitmap)
image_et.text = "Image picked!"
}
}
}
}
}
}
// Creates check mark at the top of the fragment
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.add_fragment_menu, menu)
}
// If the check mark is clicked, entry gets added
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if(item.itemId == R.id.menu_add) {
insertDataToDb()
}
return super.onOptionsItemSelected(item)
}
// uses below function to check if text is empty, and gets values from add fragment
private fun insertDataToDb() {
val mTitle = title_et.text.toString()
val mMood = mood_spinner.selectedItem.toString()
val mDescription = description_et.text.toString()
val mDate = currentDate.toString()
val validation = mSharedViewModel.verifyDataFromUser(mTitle, mDescription)
if(validation) {
val newData = JournalData(
0,
mTitle,
parseMood(mMood),
mDescription,
mDate,
bitmap
)
mJournalViewModel.insertData(newData)
Toast.makeText(requireContext(), "New entry added!", Toast.LENGTH_SHORT).show()
findNavController().navigate(R.id.action_addFragment_to_listFragment) // Jetpack Navigation
} else {
Toast.makeText(requireContext(), "You have some empty fields.", Toast.LENGTH_SHORT).show()
}
}
And this is the database these values are getting inserted into:
#Entity(tableName = "journal_table")
#Parcelize
data class JournalData(
#PrimaryKey(autoGenerate = true)
var id: Int,
var title: String,
var mood: Mood,
var description: String,
var date: String,
var image: Bitmap
): Parcelable
Issue
If the user doesn't pick an image, my app crashes and doesn't work in general (as expected). Is there any way to make it so that the bitmap/image in each entry can just stay as null or empty when all the other values are being inserted without causing any errors?
Basically, I'm trying to make attaching an image when making a journal entry optional, but I'm not sure how I could do this.
I'm pretty new to using room databases and have been trying to figure out a solution for this for a few days now, so any help would be greatly appreciated!
If your image is not taken then you can stop the insertion by adding check before db insertion OR if you want to insert even if the image is null then you can simply add the default value of bitmap in room data entity class
var image: Bitmap?=null
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
}
}
}
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.