Kotlin Multiplatform app crashes on start - android-studio

When I run my app, it is supposed to show a screen for a user to create an account or sign in. I had the code working as a regular kotlin app and decided to make a multiplatform app instead so I remade the project as multiplatform. KMM does not support this code:
import kotlinx.android.synthetic.main.activity_sign_in.*
so I had to change to ViewBinding. Here is the code I used previously and what I changed it to:
Before:
import android.content.Intent
import android.os.Bundle
import android.widget.EditText
import com.cj.globekotlin.Extensions.toast
import com.cj.globekotlin.FirebaseUtils.firebaseAuth
import kotlinx.android.synthetic.main.activity_sign_in.*
class SignInActivity : AppCompatActivity() {
lateinit var signInEmail: String
lateinit var signInPassword: String
lateinit var signInInputsArray: Array<EditText>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sign_in)
signInInputsArray = arrayOf(etSignInEmail, etSignInPassword)
btnCreateAccount2.setOnClickListener {
startActivity(Intent(this, CreateAccountActivity::class.java))
finish()
}
btnSignIn.setOnClickListener {
signInUser()
}
}
private fun notEmpty(): Boolean = signInEmail.isNotEmpty() && signInPassword.isNotEmpty()
private fun signInUser() {
signInEmail = etSignInEmail.text.toString().trim()
signInPassword = etSignInPassword.text.toString().trim()
if (notEmpty()) {
firebaseAuth.signInWithEmailAndPassword(signInEmail, signInPassword)
.addOnCompleteListener { signIn ->
if (signIn.isSuccessful) {
startActivity(Intent(this, HomeActivity::class.java))
toast("signed in successfully")
finish()
} else {
toast("sign in failed")
}
}
} else {
signInInputsArray.forEach { input ->
if (input.text.toString().trim().isEmpty()) {
input.error = "${input.hint} is required"
}
}
}
}
}
Current:
import android.content.Intent
import android.os.Bundle
import android.widget.EditText
import com.cj.globemultiplatform.android.Extensions.toast
import com.cj.globemultiplatform.android.FirebaseUtils.firebaseAuth
import com.cj.globemultiplatform.android.databinding.ActivitySignInBinding
class SignInActivity : AppCompatActivity() {
lateinit var signInEmail: String
lateinit var signInPassword: String
lateinit var signInInputsArray: Array<EditText>
private lateinit var binding: ActivitySignInBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySignInBinding.inflate(layoutInflater)
setContentView(R.layout.activity_sign_in)
signInInputsArray = arrayOf(binding.etSignInEmail, binding.etSignInPassword)
binding.btnCreateAccount2.setOnClickListener {
startActivity(Intent(this, CreateAccountActivity::class.java))
finish()
}
binding.btnSignIn.setOnClickListener {
signInUser()
}
}
private fun notEmpty(): Boolean = signInEmail.isNotEmpty() && signInPassword.isNotEmpty()
private fun signInUser() {
signInEmail = binding.etSignInEmail.text.toString().trim()
signInPassword = binding.etSignInPassword.text.toString().trim()
if (notEmpty()) {
firebaseAuth.signInWithEmailAndPassword(signInEmail, signInPassword)
.addOnCompleteListener { signIn ->
if (signIn.isSuccessful) {
startActivity(Intent(this, HomeActivity::class.java))
toast("signed in successfully")
finish()
} else {
toast("sign in failed")
}
}
} else {
signInInputsArray.forEach { input ->
if (input.text.toString().trim().isEmpty()) {
input.error = "${input.hint} is required"
}
}
}
}
}
Initially, I thought that this would work but now whenever I open the app, it crashes. I think this change is what's causing the crashes. Is that possible? If so, how can I fix this?

There are a couple of problems with your android code:
There is no connection between your inflated binding and the content you are setting, you should use
binding = ActivitySignInBinding.inflate(layoutInflater)
setContentView(binding.root)
Your lateinit variables for the inputs are used in the signInUser() method, but it's not initialised. I'd suggest removing that and using binding.signInEmail and the other views.
Also, if you're trying to share code between Android & iOS, you should be aware that you'll need to abstract away any platform specific implementation. For ex: all packages that are android specific will not work on iOS at this moment. This specific code is pretty Android platform heavy, thus I wouldn't even try to share this in a KMM app, only business logic, perhaps up to a ViewModel layer.

Related

can't use a toast message in the recyclerview

I am making recyclerview in kotlin in android studio. I'm trying to set up an event listener that puts a button in the recyclerview and outputs a toast message. Even if this#[activity name] is entered instead of this in context, a toast message is not displayed. What went wrong?
The error code is as follows.
Unresolved reference: #UpdateActivity
class UpdateActivity : AppCompatActivity() {
private val vBinding by lazy {ActivityUpdateBinding.inflate(layoutInflater)}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(vBinding.root)
var recyclerViewAdapter = CustomAdapter()
recyclerViewAdapter.listData = arrayListOf<String>("a", "b", "c")
vBinding.uploadedRecyclerView.adapter = recyclerViewAdapter
vBinding.uploadedRecyclerView.layoutManager = LinearLayoutManager(this)
} // onCreate End
class CustomAdapter:RecyclerView.Adapter<CustomAdapter.Holder>(){
var listData = arrayListOf<String>()
class Holder(val vBinding:UploadedRecyclerBinding):RecyclerView.ViewHolder(vBinding.root){
fun setData(name:String){
vBinding.name.text = name
vBinding.getBtn.setOnClickListener {
**Toast.makeText(this#UpdateActivity, "test", Toast.LENGTH_SHORT).show()**
// ↑A compilation error occurs here.**
}
}
} // Holder end
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val vBinding = UploadedRecyclerBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return Holder(vBinding)
}
override fun onBindViewHolder(holder: Holder, position: Int) {
val name = listData[position]
holder.setData(name)
}
override fun getItemCount(): Int {
return listData.size
}
} // CustomAdapter end
} // UpdateActivity end
you could try put context into constructor of adapter when you create it, and use this context to show a toast. something like this:
class CustomAdapter(
private val mContext: Context
):RecyclerView.Adapter<CustomAdapter.Holder>(){
and in holder class:
Toast.makeText(mContext, "test", Toast.LENGTH_SHORT).show()
finally, when you create adapter in activity:
var recyclerViewAdapter = CustomAdapter(this)
Normally, we'll create adapter class in another file, so use this#UpdateActivity in adapter is bad idea
Instead of using the local context in Toast, always use applicationContext when showing it. So, you should replace that creation of Toast message as,
Toast.makeText(applicationContext, "test", Toast.LENGTH_SHORT).show()

Invisible mode in Android studio (kotlin)

I'm working on a news app and I want my layout to become invisible when a user saves an article. Can anyone please tell me the right way to write this code?
2 things to note:
-when I run the app,the layout is visible in the "saved fragment"
but then when I add "hideSavedMessage" right next to the code that updates the recyclerView and I run the app, the layout becomes invisible.
I want the layout to be invisible only when the user saves an article.
PS: I know how the visible and invisible mode works. I have used it before. My major problem is not knowing the right place to write the code. And by layout, I mean the text view and image view that appears on the screen. I would appreciate any contributions. Thank you.
Here's my code
class SavedFragment : Fragment(R.layout.fragment_saved) {
lateinit var viewModel: NewsViewModel
lateinit var newsAdapter: SavedAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = (activity as NewsActivity).viewModel
setupRecyclerView()
newsAdapter.setOnItemClickListener {
val bundle = Bundle().apply {
putSerializable("article", it)
}
findNavController().navigate(
R.id.action_savedFragment_to_savedArticleFragment,
bundle
)
}
val itemTouchHelperCallback = object : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position =
viewHolder.adapterPosition//get position of item we deleted so that we swipe to left or right
val article =
newsAdapter.differ.currentList[position]//from news adapter at the index of the position
viewModel.deleteArticle(article)
Snackbar.make(view, "Successfully deleted article", Snackbar.LENGTH_LONG)
.apply {
setAction("Undo") {
viewModel.saveArticle(article); hideSavedMessage()
}
show()
}
val isAtLastItem = position <= 0
val shouldUpdateLayout = isAtLastItem
if (shouldUpdateLayout) {
showSavedMessage()
}
}
}
ItemTouchHelper(itemTouchHelperCallback).apply {
attachToRecyclerView(rvSavedNews)
}
viewModel.getSavedNews().observe(viewLifecycleOwner, Observer { articles ->
newsAdapter.differ.submitList(articles)
})
}
private fun setupRecyclerView() {
newsAdapter = SavedAdapter()
rvSavedNews.apply {
adapter = newsAdapter
layoutManager = LinearLayoutManager(activity)
}
}
private fun hideSavedMessage() {
savedMessage.visibility = View.INVISIBLE
isArticleAdded = false
}
private fun showSavedMessage() {
savedMessage.visibility = View.VISIBLE
}
}
The problem is that code inside observer runs even at the beginning - when you run your app, right? If I understand your problem, you just have to manage to make the fun hideSavedMessage() not be running for the first time. You could for example instantiate a boolean in onCreate() and set it to false. Then, inside the observer, you could run the hideSavedMessage() only if that boolean is true - you would set it as true at the end of the observer. I hope you understand.

E/RecyclerView: No adapter attached; skipping layout / Kotlin

I have been reading the different answers here on stackoverflow and on this blog post and tried to implement their solutions but I am still getting the error. The code is from a yt tutorial. I hope someone can help me. Thanks
Error E/RecyclerView: No adapter attached; skipping layout
My Adapter
This is my Main Activity
class MainActivity : AppCompatActivity(), IDrinkLoadListener {
lateinit var drinkLoadListener: IDrinkLoadListener
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
init()
loadDrinkFromFirebase()
}
private fun loadDrinkFromFirebase() {
val drinkModels : MutableList<DrinkModel> = ArrayList()
FirebaseDatabase.getInstance()
.getReference("Drink")
.addListenerForSingleValueEvent(object: ValueEventListener{
override fun onDataChange(snapshot: DataSnapshot) {
if(snapshot.exists()) {
for (drinkSnapshot in snapshot.children) {
val drinkModel = drinkSnapshot.getValue(DrinkModel::class.java)
drinkModel!!.key = drinkSnapshot.key
drinkModels.add(drinkModel)
}
drinkLoadListener.onDrinkLoadSuccess(drinkModels)
} else
drinkLoadListener.onDrinkLoadFailed("Drink items not exist")
}
override fun onCancelled(error: DatabaseError) {
drinkLoadListener.onDrinkLoadFailed(error.message)
}
})
}
private fun init() {
drinkLoadListener = this
val gridLayoutManager = GridLayoutManager(this, 2)
recycler_drink.layoutManager = gridLayoutManager
recycler_drink.addItemDecoration(SpaceItemDecoration())
}
override fun onDrinkLoadSuccess(drinkModelList: List<DrinkModel>?) {
val adapter = MyDrinkAdapter(this,drinkModelList!!)
recycler_drink.adapter = adapter
}
override fun onDrinkLoadFailed(message: String?) {
Snackbar.make(mainLayout,message!!,Snackbar.LENGTH_LONG).show()
}
}
class SpaceItemDecoration : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
if(parent.getChildLayoutPosition(view) % 2 != 0)
{
outRect.top= 25
outRect.bottom= -25
}else outRect.top = 0
}
}
Thanks in advance!!!
That happens because the adapter is attached to the recycler after an asynchronous operation on onDrinkLoadSuccess
You can attach the adapter to the recycler during onCreate and then the onDrinkLoadSuccess update it with the data.
The ListAdapter class has a submit method for it. If you are using plain recycler is usually something like this:
Recycler ... {
private val dataList = mutableListOf()
fun update(list: List<YourType>) {
dataList.clear()
dataList.addAll(list)
notifyDataDetChanged() //important updates the UI
}
}
There are more specific updates methods like notifyItemRangeChanged, etc.
Also there are helpers for finding difference in the data sets DiffUtil.ItemCallback

How to make a sequence of two animations in Kotlin?

I have to animations in my Kotlin Scene and I want to make a sequence out of these two and I want the sequence to have infinite repetitions like looping the sequence.
//First Animation
ObjectAnimator.ofFloat(block, "translationX", 50f).apply {
duration = 500
start()
}
//Second Animation
ObjectAnimator.ofFloat(block, "translationX", 0f).apply {
duration = 500
start()
}
Thanks for your help!
Solution:
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.view.WindowManager
import kotlinx.android.synthetic.main.activity_main.*
import android.animation.ValueAnimator
import android.animation.ObjectAnimator
import android.animation.AnimatorSet
import android.animation.Animator
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
animateTogether(
stripe.objectAnimate() // https://github.com/blipinsk/ViewPropertyObjectAnimator
.translationX(100f)
.get(),
stripe.objectAnimate()
.translationX(-100f)
.get()
).start()
}
fun animateTogether(vararg animators: Animator): AnimatorSet =
AnimatorSet().apply {
ObjectAnimator.ofFloat(stripe, "translationX", 100f).apply {
duration = 500
start()
}
ObjectAnimator.ofFloat(stripe, "translationX", -100f).apply {
duration = 500
start()
}
}
Try AnimatorSet
AnimatorSet as = new AnimatorSet();
as.playSequentially(ObjectAnimator.ofFloat(...),
ObjectAnimator.ofFloat(...));
then you have two ways to loop it one set repeat mode on it's each animators taken from here
objectAnimator.setRepeatCount(ObjectAnimator.INFINITE);
objectAnimator.setRepeatMode(ObjectAnimator.RESTART/REVERSE...);
second use AnimatorListenerAdapter to listen when animation ends and restart the same animation.
mAnimationSet.addListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mAnimationSet.start();
}
});
mAnimationSet.start();
I like to do the following:
fun View.objectAnimate() = ViewPropertyObjectAnimator.animate(this) // https://github.com/blipinsk/ViewPropertyObjectAnimator
fun animateTogether(vararg animators: Animator): AnimatorSet =
AnimatorSet().apply {
playTogether(*animators)
}
And now I can do
animateTogether(
someView.objectAnimate()
.translationX(40.dp)
.get(),
otherView.objectAnimate()
.translationX(-40.dp)
.get()
).start()
For sequence, you can play with .duration and .initialDelay values of Animator, and can set repeat mode, etc.

Can't communication between DialogFragment and Activity using Observer pattern?

When you press the button to open a separate input window, there is a function to display the results toast.
class MainActivity : AppCompatActivity() {
val disposable = CompositeDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
val f = TestPopup()
usingRxJava(f)
//usingLiveData(f)
}
}
private fun usingRxJava(f: TestPopup) {
val subject = SingleSubject.create<String>()
f.show(supportFragmentManager, "TAG")
button.post {
f.dialog.setOnDismissListener {
val str = f.arguments?.getString(TestPopup.TEST_KEY) ?: ""
subject.onSuccess(str)
}
}
subject.subscribe({
Toast.makeText(this, "Accept : $it", Toast.LENGTH_SHORT).show()
}, {
}).addTo(disposable)
}
private fun usingLiveData(f: TestPopup) {
val liveData = MutableLiveData<String>()
f.show(supportFragmentManager, "TAG")
button.post {
f.dialog.setOnDismissListener {
val str = f.arguments?.getString(TestPopup.TEST_KEY) ?: ""
liveData.postValue(str)
}
}
liveData.observe(this, Observer {
Toast.makeText(this, "Accept : $it", Toast.LENGTH_SHORT).show()
})
}
override fun onDestroy() {
disposable.dispose()
super.onDestroy()
}
}
DialogFragment
class TestPopup : DialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.dialog_test, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
button_test.setOnClickListener {
val arg = Bundle()
arg.putString(TEST_KEY, edit_test.text.toString())
arguments = arg
dismiss()
}
}
companion object {
const val TEST_KEY = "KEY"
}
}
(Sample Project Url : https://github.com/heukhyeon/DialogObserverPattern )
This sample code works in normal cases. However, the toast does not float after the following procedure.
Developer Option - Dont'keep activities enable
Open TestPopup, and enter your text. (Do not press OK button)
Press the home button to move the app to the background
The app is killed by the system.
Reactivate the app (Like clicking on an app in the apps list)
In this case, the text I entered remains on the screen, but nothing happens when I press the OK button.
Of course I know this happens because at the end of the activity the observe relationship between the activity and the Dialog is over.
Most of the code uses the implementation of the callback interface for that Dialog in the Activity to handle that case.
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
button_test.setOnClickListener {
val input = edit_test.text.toString()
(activity as MyListener).inputComplete(input)
dismiss()
}
}
class MainActivity : AppCompatActivity(), TestPopup.MyListener {
override fun inputComplete(input: String) {
Toast.makeText(this, "Accept : $input", Toast.LENGTH_SHORT).show()
}
}
But I think it's a way that doesn't match the Observer pattern, and I want to implement it using the Observer pattern as much as possible.
I'm thinking of getting a Fragment from the FragmentManager and subscribing again at onCreate, but I think there's a better way.
Can someone help me?
Your understanding of the problem is correct, except that the problem happens with any configuration changes, including screen rotation. You can reproduce issue without using the developer mode. Try this for example:
Open TestPopup, and enter your text. (Do not press OK button)
Rotate screen
See toast message not popping up.
Also note that your "observer pattern" implementation is not a proper observer pattern. Observer pattern has a subject and an observer. In your implementation, the activity is acting as both the subject and the observer. The dialog is not taking any part in this observer pattern, and using .setOnDismissListener is just another form of a listener pattern.
In order to implement observer pattern between the Fragment(the subject) and the Activity(the observer), the Activity needs to get the reference of the Fragment using the FragmentManager as you suggested. I suggest to use view model and establish observer pattern between view layer and view model layer instead.
RxJava example:
//MainViewModel.kt
class MainViewModel: ViewModel() {
val dialogText = PublishProcessor.create<String>()
fun postNewDialogText(text: String) {
dialogText.onNext(text)
}
}
// Activity
val disposable = CompositeDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
viewModel.dialogText.subscribe {
Toast.makeText(this, "Accept : $it", Toast.LENGTH_SHORT).show()
}.addTo(disposable)
button.setOnClickListener {
TestPopup().show(supportFragmentManager, "TAG")
// usingRxJava(f)
// usingLiveData(f)
}
}
override fun onDestroy() {
disposable.dispose()
super.onDestroy()
}
// Dialog Fragment
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// Important!! use activity when getting the viewmodel.
val viewModel = ViewModelProviders.of(requireActivity()).get(MainViewModel::class.java)
button_test.setOnClickListener {
viewModel.postNewDialogText(edit_test.text.toString())
dismiss()
}
}

Resources