Kotlin Composable: How to return data from a called activity - kotlin

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()

Related

How to make functions wait result

I'm newbie in coding, so I want to ask more experienced programmers how to do it right.
I have 2 functions, first from Facebook SDK and second from AppsFlyerLib.
Can you tell me if there is right option to wait results from this functions please.
Here example of code:
class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
var linkdataPlusApps = ""
//Getting Link Data
val linkdataHandler = object : AppLinkData.CompletionHandler {
override fun onDeferredAppLinkDataFetched(appLinkData: AppLinkData?) {
linkdataPlusApps += appLinkData.toString()
}
}
AppLinkData.fetchDeferredAppLinkData(this, linkdataHandler)
//Gettings Apps
val appsdataHandler = object : AppsFlyerConversionListener {
override fun onConversionDataSuccess(p0: MutableMap<String, Any>?) {
linkdataPlusApps += p0.toString()
}
override fun onConversionDataFail(p0: String?) {}
override fun onAppOpenAttribution(p0: MutableMap<String, String>?) {}
override fun onAttributionFailure(p0: String?) {}
}
AppsFlyerLib.getInstance().init("APPS_KEY", appsdataHandler, this).start(this)
//Here I wanna take this data and put in another activity
val intent = Intent(this, NextActivity::class.java)
intent.putExtra("FetchedData", linkdataPlusApps)
startActivity(intent)
}}
But this code doesn't work because Activity started before data was fetched so string is empty.
I solved this by chaining code, but I'm sure that is dirty-coding.
How I did
class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
var linkdataPlusApps = ""
//Getting Link Data
val linkdataHandler = object : AppLinkData.CompletionHandler {
override fun onDeferredAppLinkDataFetched(appLinkData: AppLinkData?) {
linkdataPlusApps += appLinkData.toString()
val appsdataHandler = object : AppsFlyerConversionListener {
override fun onConversionDataSuccess(p0: MutableMap<String, Any>?) {
linkdataPlusApps += p0.toString()
val intent = Intent(this#MainActivity, NextActivity::class.java)
intent.putExtra("FetchedData", linkdataPlusApps)
startActivity(intent)
}
override fun onConversionDataFail(p0: String?) {}
override fun onAppOpenAttribution(p0: MutableMap<String, String>?) {}
override fun onAttributionFailure(p0: String?) {}
}
AppsFlyerLib.getInstance().init("APPS_KEY", appsdataHandler, this#MainActivity).start(this#MainActivity)
}
}
AppLinkData.fetchDeferredAppLinkData(this, linkdataHandler)
}
}
So code starts from fetching AppLinkData, and when fetched, starting fetching apps, and only then starting second activity with right string.
Can I do it in another way?

Getting list from viewmodel in observe event -MVVM

I have a issue in getting a list returned in observe event in my activity. i am developing a login screen in MVVM. viewmodel is as follows.
my problem is i can get returned data in observe call back into a UI control. but same data returned assign into a list variable is empty. in other words, list returned unable to pass into a a list variable in an activity.
class LoginViewModel #Inject internal constructor (private val loginRepository: LoginRepository,private val usersRepository: UsersRepository): ViewModel() {
private var _userEmail:MutableLiveData<String>
private var _userPassword:MutableLiveData<String>
private var _userLoginData:MutableLiveData<UserLoginData>
private var allUsers:MutableLiveData<List<Users>>
private var findUser:MutableLiveData<List<Users>>
init{
_userEmail= MutableLiveData()
_userPassword= MutableLiveData()
_userLoginData= MutableLiveData()
allUsers= MutableLiveData()
findUser= MutableLiveData()
}
fun getEmail():LiveData<String>{
return _userEmail
}
fun getPassword():MutableLiveData<String>{
return _userPassword
}
fun userLogin(userEmail:String,userPassword:String):MutableLiveData<UserLoginData>{
_userEmail.postValue(userEmail)
_userPassword.postValue(userPassword)
viewModelScope.launch(Dispatchers.IO) {
var userlogindata:UserLoginData=loginRepository.userLogin(userEmail,userPassword)
_userLoginData.postValue(userlogindata)
}
return _userLoginData
}
fun getAllUsers():MutableLiveData<List<Users>>{
//lateinit var _allUsers:List<Users>
viewModelScope.launch(Dispatchers.IO) {
val _allUsers:List<Users> =usersRepository.getUsers()
allUsers.postValue(_allUsers)
}
return allUsers
}
fun findUser(userEmail:String):MutableLiveData<List<Users>>{
//lateinit var finduser:List<Users>
viewModelScope.launch(Dispatchers.IO) {
val _findUser:List<Users> =usersRepository.findUser(userEmail)
findUser.postValue(_findUser)
}
return findUser
}
}
in an activity i am observing the users list and getting the list into a list variable in the activity. code in the activity:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var loginViewModel: LoginViewModel
lateinit var loginData:UserLoginData
var users:List<Users> = emptyList()
var findUser:List<Users> = emptyList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
loginViewModel = ViewModelProvider(this).get(LoginViewModel::class.java)
/*observe users list*/
loginViewModel.getAllUsers().observe(this, {It->
users=It
binding.textView.text=It[0].email.toString()
})
loginViewModel.findUser(binding.loginEditTextTextEmailAddressTxt.toString().trim()).observe(this,{it->
findUser=it
})
This program failed if i use data in the users or findUser lists.
Kindly help me to find the best practice in getting the changed data from viewmodel into an activity
ViewModel:
data class User(
var name: String
)
private val _allUsers = MutableLiveData<List<User>>()
private val allUsers: LiveData<List<User>> get() = _allUsers
fun fetchAllUsers(): LiveData<List<User>> {
viewModelScope.launch {
//delay is simulating network request delay
delay(1000)
//listOf is simulating usersRepository.getUsers()
_allUsers.value = listOf(User("name1"), User("name2"), User("name3"))
}
return allUsers
}
Fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.fetchAllUsers().observe(viewLifecycleOwner) { userList ->
userList.forEach {
Log.d("user", it.name)
}
}
You can try this way but I do not prefer returning liveData with function because you have to observe liveData once. You need to be careful observe once.

RecyclerView in Fragment not populating

So I've been wrestling with this for days and I need some help. I've made this code work in an activity, but then I move it to a fragment it doesn't work. Everything else is the same between the two.
Using the debugger with the working Activity, the line
apiService = retrofit.create<HomeJsonApiService>(HomeJsonApiService::class.java)
goes to getItemCount(). However in the fragment it goes directly to onCreateView in the Fragment. I've attached my code below. Thanks in advance for the help! And be gentle. I'm still new to this :)
First is my fragment:
class TabHomeActivity : Fragment() {
val itemList = ArrayList<HomeCards>()
lateinit var adapter: HomeCardsAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var binding = FragmentTabHomeActivityBinding.inflate(layoutInflater)
adapter = HomeCardsAdapter()
var rv = binding.rvHomeCards
rv.adapter = adapter
loadData()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.cards_home, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
private fun loadData() {
ApiManager.getInstance().service.listHeroes()
.enqueue(object : Callback<ResponseData<List<HomeCards>>> {
override fun onResponse(
call: Call<ResponseData<List<HomeCards>>>,
response: Response<ResponseData<List<HomeCards>>>
) {
val listData: List<HomeCards> = response.body()!!.data
// updating data from network to adapter
itemList.clear()
itemList.addAll(listData)
adapter.updateData(itemList)
adapter.notifyDataSetChanged()
}
override fun onFailure(call: Call<ResponseData<List<HomeCards>>>, t: Throwable) {
}
})
}
}
The HTTP request:
data class ResponseData<T> (
val code: Int,
val data: T
)
interface HomeJsonApiService {
#GET("marvel-heroes.asp?h=2")
fun listHeroes(): retrofit2.Call<ResponseData<List<HomeCards>>>
}
class ApiManager {
private var apiService: HomeJsonApiService? = null
init {
createService()
}
val service: HomeJsonApiService get() = apiService!!
private fun createService() {
val loggingInterceptor =
HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {
override fun log(message: String) {
Log.i("Retrofit", message)
}
})
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
val client = OkHttpClient.Builder()
.readTimeout(30, TimeUnit.SECONDS)
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.addInterceptor(loggingInterceptor)
.build()
val retrofit: Retrofit = Retrofit.Builder()
.client(client)
.baseUrl("https://www.mywebsite.com/jsonfolder/JSON/")
.addConverterFactory(GsonConverterFactory.create())
.build()
apiService = retrofit.create(HomeJsonApiService::class.java)
}
companion object {
private var instance: ApiManager? = null
fun getInstance(): ApiManager {
return instance ?: synchronized(this) {
ApiManager().also { instance = it }
}
}
}
}
And my adapter:
class HomeCardsAdapter() : RecyclerView.Adapter<HomeCardsAdapter.ViewHolder>() {
private lateinit var itemList: List<HomeCards>
lateinit var context: Context
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
context = parent.context
val view = LayoutInflater.from(context).inflate(R.layout.cards_home, parent, false)
return ViewHolder(view)
}
override fun getItemCount(): Int {
return if (::itemList.isInitialized) itemList.size else 0
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind()
}
fun updateData(list: List<HomeCards>) {
itemList = list;
notifyDataSetChanged()
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
//var binding = ActivityMainBinding(layoutInflater(inf))
fun bind() {
val item = itemList.get(adapterPosition)
ViewHolder(itemView).itemView.findViewById<TextView>(R.id.cardHomeTitle).text = item.name
ViewHolder(itemView).itemView.findViewById<TextView>(R.id.cardHomeTitle).text = item.superheroName
Glide.with(context)
.load(item.photo)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.circleCrop()
.into(ViewHolder(itemView).itemView.findViewById<ImageView>(R.id.cardHomeIcon))
}
}
}
class HomeCards {
#SerializedName("superhero_name")
var superheroName: String = ""
var name: String = ""
var photo: String = ""
}
The main problem is:
var binding = FragmentTabHomeActivityBinding.inflate(layoutInflater)
That is inside on onCreate but onCreateView is returning another view inflater.inflate(R.layout.cards_home, container, false)
So you are applying the adapter to a recycler that is on the binding, but the view on the screen is inflated from the layout. Change it to this:
private lateinit var binding: FragmentTabHomeActivityBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View? {
binding = FragmentTabHomeActivityBinding.inflate(layoutInflater, container, false)
return binding.root
}
And move the code from from onCreate to onViewCreated but make sure to use the lateinit binding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = HomeCardsAdapter()
var rv = binding.rvHomeCards
rv.adapter = adapter
loadData()
}
After that there is a problem in your adapter: private lateinit var itemList: List<HomeCards> very specifically List<HomeCards>. The method notifyDataSetChanged doesn't work by changing or updating the reference of the data structure but when the collection is modified. Change it to this:
private val list = mutableListOf<HomeCards>()
override fun getItemCount(): Int {
return list.size()
}
fun updateData(list: List<HomeCards>) {
this.itemList.clear()
this.itemList.addAll(list)
notifyDataSetChanged()
}
If onResponse() gets called and provides response, verify that code updating UI is running on main/ui thread. Common source of issue when working with network (other threads).
activity?.runOnUiThread {
itemList.clear()
itemList.addAll(listData)
adapter.updateData(itemList)
adapter.notifyDataSetChanged()
}

RecyclerView AsyncListDiffer and data source consistency state lose with onClickListener

I have data source(in that example it's just a var myState: List)
class MainActivity : AppCompatActivity() {
var generation: Int = 0
var myState: List<User> = emptyList()
val userAdapter = UserAdapter {
val index = myState.indexOf(it)
if (index == -1)
println("🔥 not found")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recycler = findViewById<RecyclerView>(R.id.rvContent)
recycler.layoutManager = LinearLayoutManager(this)
recycler.adapter = userAdapter
Thread {
while (true) {
generateNewData()
Handler(mainLooper).post {
userAdapter.submit(myState)
}
sleep(3000L)
}
}.start()
}
fun generateNewData() {
generation++
myState = (0..5000).map { User("$generation", it) }
}
}
I have RecyclerView, and AsyncListDiffer connected to it
data class User(val name: String, val id: Int) {
val createdTime = System.currentTimeMillis()
}
data class UserViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
fun bindTo(user: User, action: (User) -> Unit) {
val textView = view.findViewById<TextView>(R.id.title)
textView.text = "${user.name} ${user.id} ${user.createdTime}"
textView.setOnClickListener { action(user) }
}
}
class UserAdapter(val action: (User) -> Unit) : RecyclerView.Adapter<UserViewHolder>() {
val differ: AsyncListDiffer<User> = AsyncListDiffer(this, DIFF_CALLBACK);
object DIFF_CALLBACK : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem == newItem
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
return UserViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false))
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val user = differ.currentList[position]
holder.bindTo(user, action = action)
}
fun submit(list: List<User>) {
differ.submitList(list)
}
override fun getItemCount(): Int {
return differ.currentList.size
}
}
I have OnClickListener binded to every item on RecyclerView
{
val index = myState.indexOf(it)
if (index == -1)
println("🔥 not found")}
That listener checks if item that was clicked is exists in the data source, and if not, outputs it to the console.
Every few seconds data in the source are changed, and pushed to
a AsyncListDiffer via submitList method, some how internally it uses other thread to match data and pass that diffed data
to the RecyclerView, and that takes some time;
If I starts clicking on the items non-stop, and the click event occurs at the same time when the differ inserts new data, then I get into a non-consistent state.
So, how to handle that?
Ignore a click with inconsistent data?(cons: User can see some strange behaviour like list item not collapse/expand, no navigation happen, etc)
Try to find a similar item in the new data by separate fields(positions/etc), and use it?(cons: same as 1. but less probability)
Block OnClickListener events until the data is consistent in both the Recycler and the data source? (cons: same as above, and also lag with action user performed until data became consistent again)
Something else? What is a best way to solve that?

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()
}
}