Check if an application is on its first run with Kotlin - android

Basically, I want to have a screen/view that will open when the user opens up the app for the first time. This will be a login screen type of thing.
there are classes SplashActivity
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//hiding title bar of this activity
window.requestFeature(Window.FEATURE_NO_TITLE)
//making this activity full screen
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
setContentView(R.layout.activity_splash)
//4second splash time
Handler().postDelayed({
//start main activity
startActivity(Intent(this#SplashActivity, MyCustomAppIntro::class.java))
//finish this activity
finish()
},2000)
}
}
class MyCustomAppIntro
class MyCustomAppIntro : AppIntro() {
companion object {
fun startActivity(context: Context) {
val intent = Intent(context, MyCustomAppIntro::class.java)
context.startActivity(intent)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTransformer(AppIntroPageTransformerType.Depth)
// You can customize your parallax parameters in the constructors.
setTransformer(AppIntroPageTransformerType.Parallax(
titleParallaxFactor = 1.0,
imageParallaxFactor = -1.0,
descriptionParallaxFactor = 2.0
))
// Make sure you don't call setContentView!
// Call addSlide passing your Fragments.
// You can use AppIntroFragment to use a pre-built fragment
addSlide(
AppIntroFragment.newInstance(
imageDrawable = R.drawable.ayana,
backgroundDrawable = R.color.black,
description = "Привет мой друг"
))
addSlide(
AppIntroFragment.newInstance(
imageDrawable = R.drawable.ayana,
backgroundDrawable = R.color.black,
description = "Меня зовут AYANA"
))
addSlide(
AppIntroFragment.newInstance(
backgroundDrawable = R.drawable.screen_3
))
}
override fun onSkipPressed(currentFragment: Fragment?) {
super.onSkipPressed(currentFragment)
// Decide what to do when the user clicks on "Skip"
val intent = Intent(this,MainActivity::class.java)
startActivity(intent);
finish()
}
override fun onDonePressed(currentFragment: Fragment?) {
super.onDonePressed(currentFragment)
val intent = Intent(this,MainActivity::class.java)
startActivity(intent);
finish()
}
class MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_activity_main)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
about.setOnClickListener{
val intent = Intent(this,ScondActivity::class.java)
startActivity(intent);
}
val assistantFragment = AimyboxAssistantFragment()
supportFragmentManager.beginTransaction().apply {
replace(R.id.assistant_container, assistantFragment)
commit()
}
}
override fun onBackPressed() {
val assistantFragment = (supportFragmentManager.findFragmentById(R.id.assistant_container)
as? AimyboxAssistantFragment)
if (assistantFragment?.onBackPressed() != true) super.onBackPressed()
}
}
We suggest to don't declare MyCustomAppIntro as your first Activity unless you want the intro to launch every time your app starts. Ideally you should show the AppIntro activity only once to the user, and you should hide it once completed (you can use a flag in the SharedPreferences) ????

Use SharedPreference for that Issue, its quite easy and simple approach:
Splash Activity:
SharedPreference sharedpref = getApplicationContext.getSharedPreferences(SETTINGS_PREFERENCES,MODE_PRIVATE);
String token = sharedPreferences.getString("token", null);
if (token.equals("False") || token == null){
//will call the view for the first and last time until cache is cleared
//after trigger this login programmatically set the token value true thus you can solve the problem
SharedPreferences.Editor editor = sharedpref.edit();
editor.putString("token", "True");
editor.apply();
}else{
//do something or redirect to login or main activity
}

as #EmonHossainMunna suggested, SharedPreferences are the go, kotlin code will be as follows
val sharedpref: SharedPreferences =
getApplicationContext().getSharedPreferences(
"com.example.android.your_application",
MODE_PRIVATE
)
val token: String? = sharedpref.getString("token", null)
if (token == "False" || token == null) {
// rest of the FirstTime Logic here
sharedpref.edit().putString("token", "true").apply()
} else {
// rest of the Not-FirstTime Logic here
}

You also may want to check if onCreate() runs the first time:
if (savedInstanceState == null) {
...
}

Related

Reload activity and call a function with the same button in Android Studio (Kotlin)

I want to create a button that allows me to both reload my activity and call a new function once the activity is reloaded. Unfortunately by calling two functions at the same time the second function I call after the activity refreshes does not work. How can I solve this problem which seems simple at first sight
fun newPie(valeur: Double){
config.addData(SimplePieInfo(valeur, Color.parseColor("#000000")))
config.drawText(false)
config.strokeMode(false)
anim.applyConfig(config)
anim.start()}
fun refresh() {
val intent = Intent(applicationContext, anychart::class.java)
startActivity(intent)
finish()
}
button.setOnClickListener(){
refresh()
newPie(valeur = 33.3)
}
If you want to call the new function once activity is reloaded, you should call that function into onCreate method of activity.
override fun onCreate(...) {
...
newPie(valeur = 33.3)
}
and button should only be used for refreshing the activity:
fun refresh() {
val intent = Intent(applicationContext, anychart::class.java)
startActivity(intent)
finish()
}
button.setOnClickListener(){
refresh()
}
As you want to restart the activity and hit the function then you should finish the activity, pass data to new instance of the activity so that you can check and trigger the function
Following code will help you
When you are finishing the activity just dont restart the activity but send some data with it as well.
var intent = Intent(this, anychart::class.java)
intent.putExtra("startFunction", true) // to trigger the function or not
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK // so any pending activity can be removed
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
Then in the onCreate function check the data and trigger function when the true is sent
intent?.extras?.let {
if(it.containsKey("startFunction")){
val isStart = it.getBoolean("Data", false)
if(isStart){
newPie(valeur = 33.3)
}
}
}
The extra function call needs to happen in the newly created Activity instance, and it has to be done after onCreate() has been called. You could directly put this function call in onCreate(), but if you don't want it to be called the very first time the Activity is opened, then you need to add an extra to your Intent and use that extra to determine if the function should be called. Like this:
companion object {
private const val IS_REFRESH_KEY = "is_refresh"
}
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
// ...
button.setOnClickListener(){
refresh()
}
val isRefresh = intent.extras?.getInt(IS_REFRESH_KEY) == 1
if (isRefresh) {
newPie(valeur = 33.3)
}
}
fun refresh() {
val intent = Intent(applicationContext, anychart::class.java).apply {
putExtra(IS_REFRESH_KEY, 1)
}
startActivity(intent)
finish()
}
If you want the 33.3 to be customizable, you could use a Float extra instead:
companion object {
private const val REFRESH_PIE_VALUE = "refresh_pie_value"
}
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
// ...
button.setOnClickListener(){
refresh()
}
val refreshPieValue = intent.extras?.getFloat(REFRESH_PIE_VALUE) ?: -1f
if (refreshPieValue >= 0f) {
newPie(refreshPieValue)
}
}
fun refresh() {
val intent = Intent(applicationContext, anychart::class.java).apply {
putExtra(REFRESH_PIE_VALUE, 33.3) // customize the value here
}
startActivity(intent)
finish()
}

how to save data from textview when going to previous activity kotlin

So I have a child activity that I press two buttons, one to increment and one to decrement. I want to save this number when I go back to the previous activity. However, I am stuck here. I tried using shared preference, however that seems to work for main to secondary activity. I tried using Activity Result and that seems way above me right now. I want my value in the textView to stay until I press a button to reset the whole thing.
This is the parent activity.
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
class TallBoys : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_tall_boys)
val btn1: Button = findViewById(R.id.button1)
val btn2: Button = findViewById(R.id.button2)
val btn3: Button = findViewById(R.id.button3)
val btn4: Button = findViewById(R.id.button4)
val btn5: Button = findViewById(R.id.button5)
val btn6: Button = findViewById(R.id.button6)
val btn7: Button = findViewById(R.id.button7)
btn1.setOnClickListener {
val intent = Intent(this, TallBoysNumbers::class.java)
}
btn2.setOnClickListener {
val intent = Intent(this, TallBoysNumbers::class.java)
startActivity(intent)
}
btn3.setOnClickListener {
val intent = Intent(this, TallBoysNumbers::class.java)
startActivity(intent)
}
btn4.setOnClickListener {
val intent = Intent(this, TallBoysNumbers::class.java)
startActivity(intent)
}
btn5.setOnClickListener {
val intent = Intent(this, TallBoysNumbers::class.java)
startActivity(intent)
}
btn6.setOnClickListener {
val intent = Intent(this, TallBoysNumbers::class.java)
startActivity(intent)
}
btn7.setOnClickListener {
val intent = Intent(this, TallBoysNumbers::class.java)
startActivity(intent)
}
}
}
This is the child activity
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
class TallBoysNumbers : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_tall_boys_numbers)
val confirmBtn: Button = findViewById(R.id.confirm_button)
val plusBtn: Button = findViewById(R.id.plus)
val textView = findViewById<TextView>(R.id.numbers)
val negBtn: Button = findViewById(R.id.negative)
var count = 0
plusBtn.setOnClickListener {
count++
textView.text = count.toString()
if (count >= 8) {
plusBtn.isEnabled = false
negBtn.isEnabled = true
}
}
negBtn.setOnClickListener {
count--
textView.text = count.toString()
if (count <= 0) {
negBtn.isEnabled = false
plusBtn.isEnabled = true
}
}
confirmBtn.setOnClickListener {
finish()
}
}
}
There are a lot of ways to do this, so I'm just going to show how to do it using SharedPreferences. The steps you need are:
In the parent activity, load the saved numbers from SharedPreferences in onResume and set the views accordingly. This needs to be in onResume so that it is also called when returning from the child activity after the numbers are modified.
Pass the key that is being edited to the child activity, so that it can save it in the correct spot in the SharedPreferences. The child activity can also use this key to load the current value so it is initialized correctly.
For example, in the parent activity
class ShowNumbersActivity : AppCompatActivity() {
private lateinit var prefs: SharedPreferences
private lateinit var txt1: TextView
private lateinit var txt2: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_show_numbers)
prefs = getSharedPreferences("MY_PREFS", Context.MODE_PRIVATE)
val btn1 = findViewById<Button>(R.id.btn1)
val btn2 = findViewById<Button>(R.id.btn2)
txt1 = findViewById(R.id.txt1)
txt2 = findViewById(R.id.txt2)
// When you click on "Button 1" you have to signal
// to the second activity which key you are editing
btn1.setOnClickListener {
val i = Intent(this, PickNumber::class.java)
i.putExtra("KEY","VAL1")
startActivity(i)
}
btn2.setOnClickListener {
val i = Intent(this, PickNumber::class.java)
i.putExtra("KEY","VAL2")
startActivity(i)
}
}
// Update the displayed state in onResume
// so that it is updated after returning from the
// child activity editing the number
override fun onResume() {
super.onResume()
val num1 = prefs.getInt("VAL1", 0)
val num2 = prefs.getInt("VAL2", 0)
txt1.text = num1.toString()
txt2.text = num2.toString()
}
}
Then in the child activity, get the "KEY" and the same SharedPreferences instance and edit it when you click "Submit"
class PickNumber : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_pick_number)
val key = intent.getStringExtra("KEY")
if( key == null ) {
finish()
}
val prefs = getSharedPreferences("MY_PREFS", Context.MODE_PRIVATE)
val txt = findViewById<EditText>(R.id.num)
val submit = findViewById<Button>(R.id.submit)
// load the current value and display it
val current = prefs.getInt(key, 0)
txt.setText(current.toString())
submit.setOnClickListener {
// Get the updated number, save it to shared preferences
// (remember to call "apply") and finish this activity.
// When onResume is called in the parent activity it will
// load this updated number.
val num = txt.text.toString()
val numI = num.toIntOrNull() ?: 0
prefs.edit().putInt(key, numI).apply()
finish()
}
}
}
If you want to clear the numbers, you could make a button that calls prefs.edit().clear().apply() to erase all the saved values, or prefs.edit().remove(someKey).apply() to erase just one specific value.
There are definitely better ways to do this - if you switch to using a Fragment like you indicated these could be stored in a shared ViewModel that handles persistence and would be a cleaner design overall.
You are calling "finish" method. This will finish TallBoysNumbers activity without saving any data. Do not use SharedPreferences for that. What you'll need is "startActivityForResult".
This will allow you to save the data before calling "finish" AND getting it from the caller activity. Take a look here:
https://developer.android.com/training/basics/intents/result
with using shared pref I hope it will come to you
class dataShared() for saving count
class DataShared(
context: Context,
userSharedPrefName:String="USER_SHARED_PREF_NAME"
) {
private val sharedPreferences: SharedPreferences =
context.getSharedPreferences(
userSharedPrefName, Context.MODE_PRIVATE
)
fun getCount(): Int {
return (sharedPreferences.getInt("count", 0))
}
fun setCount(value: Int) {
val editor = sharedPreferences.edit()
editor.putInt("count", value)
editor.apply()
}
}
plusBtn and negBtn
val data=DataShared(this)
plusBtn.setOnClickListener {
if (data.getCount()<7) {
data.setCount(data.getCount() + 1)
textView.text = data.getCount().toString()
}
}
negBtn.setOnClickListener {
if (data.getCount()>=1){
data.setCount(data.getCount()-1)
textView.text=data.getCount().toString()
}
}

Can't use SharedPreferences in intended activity

There are two classes MainActivity and PickTimeForNotif in my project. In MainActivity getSharedPreferences works just fine, i can save my data and get it back. In PickTimeForNotif, however, the same method seems to do nothing.
Here's my simplified MainActivity class:
class MainActivity : AppCompatActivity(), ChangeCupDialogFragment.StringListener {
#SuppressLint("SetTextI18n")
//this is variable i'm saving
private var drankToday = 0
//function in which i save my value to SharedPreferences
private fun saveWaterCountToInternalStorage(clickCounter: Int) {
val sharedPref = this.getSharedPreferences("something", Context.MODE_PRIVATE)
with (sharedPref.edit()){
putInt(getString(R.string.clickCount), clickCounter)
apply()
}
}
//and here i get it from there
private fun loadWaterCountToInternalStorage(): Int {
val sharedPref = this.getSharedPreferences("something", Context.MODE_PRIVATE)
return sharedPref.getInt(getString(R.string.clickCount), drankToday)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
val setupNotifButton = findViewById<Button>(R.id.setupNotifButton)
setupNotifButton.setOnClickListener{
val notifIntent = Intent(applicationContext, PickTimeForNotif::class.java)
startActivity(notifIntent)
}
}
}
In setOnClickListener i intend my second activity PickTimeForNotif, here it is.
class PickTimeForNotif: AppCompatActivity(), TimePickerFragment.OnCompleteListener {
val APP_PREFERENCES = "settings"
private val SAVED_FROM_HOUR = "SetFromHour"
private var FROM_HOUR = 99
private fun saveTimeToInternalStorage(prefName1: String, Hour:Int) {
val sharedPref = this.getSharedPreferences(APP_PREFERENCES, MODE_PRIVATE)
with (sharedPref.edit()){
putInt(prefName1, Hour)
apply()
}
}
private fun loadTimeFromInternalStorage() {
val sharedPref = this.getSharedPreferences(APP_PREFERENCES, MODE_PRIVATE)
if (sharedPref.contains(APP_PREFERENCES)) {
sharedPref.getInt(SAVED_FROM_HOUR, FROM_HOUR)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.pick_time_activity)
saveTimeToInternalStorage(SAVED_FROM_HOUR, 1)
loadTimeFromInternalStorage()
Toast.makeText(applicationContext,"$FROM_HOUR", Toast.LENGTH_SHORT).show()
}
}
In the code above i'm trying to set value (1 for example ) to a SAVED_FROM_HOUR key and then get it back and assign to FROM_HOUR variable. However, the Toast shows 99, which means that new data wasn't loaded properly. I tried putting all code from loadTimeFromInternalStorage and saveTimeToInternalStorage to onCreate, but the result is same.
I also tried checking if the Preferences file exists after i call getSharedPreferences with
if (sharedPref.contains(APP_PREFERENCES))
but it does not.
So i'm asking to explain what am i doing wrong and why i can save the data in my MainActivity, but not in the second one. Thanks alot to anyone in advance!!
In loadTimeFromInternalStorage(), you are fetching the value but not assigning to variable like this:
private fun loadTimeFromInternalStorage() {
val sharedPref = this.getSharedPreferences(APP_PREFERENCES, MODE_PRIVATE)
if (sharedPref.contains(APP_PREFERENCES)) {
FROM_HOUR = sharedPref.getInt(SAVED_FROM_HOUR, FROM_HOUR)
}
}
Also, in this line FROM_HOUR = sharedPref.getInt(SAVED_FROM_HOUR, FROM_HOUR), the last parameter in getInt() method is the default value so you should make another constant for it or supply it 0.

Start activity from other activity in Kotlin. It crashes before loading xml

In Kotlin, in loginButton.setOnClickListener function, the following codes can start ProfileActivity;
val intent=Intent(this#LoginActivity, ProfileActivity::class.java)
startActivity(intent)
finish()
From the ProfileActivity, the following codes can start TestActivity;
val intent=Intent(this#ProfileActivity, TestActivity::class.java)
startActivity(intent)
finish()
However, I want to start the TestActivity from the LoginActivity. So, I updated the codes by changing only the activity name and the codes are below:
val intent=Intent(this#LoginActivity, TestActivity::class.java)
startActivity(intent)
finish()
But, the app crashes before loading activity_test.xml. Why ?
The class in the ProfileActivity.kt is;
class profileActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_profile)
}
}
The class in the TestActivity.kt is;
class TestActivity : AppCompatActivity() {
private val QUANT = false
private val LABEL_PATH = "labels.txt"
private val INPUT_SIZE = 224
#RequiresApi(Build.VERSION_CODES.O)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null) {
var strResultUri: String? = null
strResultUri = savedInstanceState.getString(strResultUri)
} else {
setContentView(R.layout.activity_test)
textViewResult = findViewById(R.id.textViewResult)
textViewResult?.setMovementMethod(ScrollingMovementMethod())
}
}
}
var strResultUri: String? = null
strResultUri = savedInstanceState.getString(strResultUri)
What exactly you do here? Passing null inside savedInstanceState.getString() method?
Also, what do you mean by changing only the name? You mean you just changed the context in the following code?
val intent=Intent(this#LoginActivity, TestActivity::class.java)
startActivity(intent)
finish()
That code would work inside login activity for sure.
Also, is that a typing mistake. profileActivity. That's camel case (Not an accepted convention), and you call ProfileActivity::class.java. This shouldn't work. If it's just a typo, ignore it.
Most probabely your these lines cause issue:
var strResultUri: String? = null
strResultUri = savedInstanceState.getString(strResultUri)
Because you are passing strResultUri null value as parameter to getString method
It should be like this:
strResultUri = savedInstanceState.getString("yourKey")

Android fragment onCreate called twice

In my app I have two activities. The main activity that only has a search button in the Appbar and a second, searchable, activity. The second activity hold a fragment that fetches the data searched in it's onCreate call. My problem is that the fragment fetches the data twice. Inspecting the lifecycle of my activities, I concluded that the searchable activity gets paused at some point, which obviously determines the fragment to be recreated. But I have no idea what causes the activity to be paused.
Here are my activities
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val root = binding.root
setContentView(root)
//Setup the app bar
setSupportActionBar(binding.toolbar);
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
return initOptionMenu(menu, this)
}
}
fun initOptionMenu(menu: Menu?, context: AppCompatActivity): Boolean {
val inflater = context.menuInflater;
inflater.inflate(R.menu.app_bar_menu, menu)
// Get the SearchView and set the searchable configuration
val searchManager = context.getSystemService(Context.SEARCH_SERVICE) as SearchManager
(menu?.findItem(R.id.app_bar_search)?.actionView as SearchView).apply {
// Assumes current activity is the searchable activity
setSearchableInfo(searchManager.getSearchableInfo(context.componentName))
setIconifiedByDefault(false) // Do not iconify the widget; expand it by default
}
return true;
}
SearchActivity.kt
class SearchActivity : AppCompatActivity() {
private lateinit var viewBinding: SearchActivityBinding
private var query: String? = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = SearchActivityBinding.inflate(layoutInflater)
val root = viewBinding.root
setContentView(root)
// Setup app bar
supportActionBar?.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
supportActionBar?.setCustomView(R.layout.search_app_bar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
//Get the query string
if (Intent.ACTION_SEARCH == intent.action) {
intent.getStringExtra(SearchManager.QUERY).also {
//Add the query to the appbar
query = it
updateAppBarQuery(it)
}
}
//Instantiate the fragment
if (savedInstanceState == null) {
val fragment = SearchFragment.newInstance();
val bundle = Bundle();
bundle.putString(Intent.ACTION_SEARCH, query)
fragment.arguments = bundle;
supportFragmentManager.beginTransaction()
.replace(R.id.container, fragment)
.commitNow()
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
return initOptionMenu(menu, this)
}
private fun updateAppBarQuery(q: String?) {
supportActionBar?.customView?.findViewById<TextView>(R.id.query)?.apply {
text = q
}
}
}
As you can see, I am using the built in SearchManger to handle my search action and switching between activities. I haven't seen anywhere in the docs that during search, my searchable activity might get paused or anything like that. Does anyone have any idea why this happens? Thanks in advance!
edit: Here is my onCreate method for the SearchFragment:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val query = arguments?.getString(Intent.ACTION_SEARCH);
//Create observers
val searchResultObserver = Observer<Array<GoodreadsBook>> {
searchResultListViewAdapter.setData(it)
}
viewModel.getSearchResults().observe(this, searchResultObserver)
GlobalScope.launch { //Perform the search
viewModel.search(query)
}
lifecycle.addObserver(SearchFragmentLifecycleObserver())
}
Here, searchResultListViewAdapter is the adapter for a RecyclerViewand searchResult is a livedata in the view-model holding the search result
Here is the stack trace for the first call of onCreate() on SearchFragment:
And here is for the second call:
Here is the ViewModel for the SearchFragment:
class SearchViewModel() : ViewModel() {
private val searchResults: MutableLiveData<Array<GoodreadsBook>> by lazy {
MutableLiveData<Array<GoodreadsBook>>();
}
fun getSearchResults(): LiveData<Array<GoodreadsBook>> {
return searchResults;
}
// TODO: Add pagination
suspend fun search(query: String?) = withContext(Dispatchers.Default) {
val callback: Callback = object : Callback {
override fun onFailure(call: Call, e: IOException) {
// TODO: Display error message
}
override fun onResponse(call: Call, response: Response) {
// TODO: Check res status
val gson = Gson();
val parsedRes = gson.fromJson(
response.body?.charStream(),
Array<GoodreadsBook>::class.java
);
// Create the bitmap from the imageUrl
searchResults.postValue(parsedRes)
}
}
launch { searchBook(query, callback) }
}
}
I made some changes to the app since posted this and right now the search doesn't work for some reason in the main branch. This ViewModel it's from a branch closer to the time I posted this. Here is the current ViewModel, although the problem is present in this variant as well:
class SearchViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
// private val searchResults: MutableLiveData<Array<GoodreadsBook>> by lazy {
//// MutableLiveData<Array<GoodreadsBook>>();
//// }
companion object {
private const val SEARCH_RESULTS = "searchResults"
}
fun getSearchResults(): LiveData<Array<GoodreadsBook>> =
savedStateHandle.getLiveData<Array<GoodreadsBook>>(SEARCH_RESULTS)
// TODO: Add pagination
fun search(query: String?) {
val searchResults = savedStateHandle.getLiveData<Array<GoodreadsBook>>(SEARCH_RESULTS)
if (searchResults.value == null)
viewModelScope.launch {
withContext(Dispatchers.Default) {
//Handle the API response
val callback: Callback = object : Callback {
override fun onFailure(call: Call, e: IOException) {
// TODO: Display error message
}
override fun onResponse(call: Call, response: Response) {
// TODO: Check res status
val gson = Gson();
val parsedRes = gson.fromJson(
response.body?.charStream(),
Array<GoodreadsBook>::class.java
);
searchResults.postValue(parsedRes)
}
}
launch { searchBook(query, callback) }
}
}
}
}
The searchBook function just performs the HTTP request to the API, all the data manipulation is handled in the viewModel
try this way
Fragment sf = SearchFragment.newInstance();
Bundle args = new Bundle();
args.putString(Intent.ACTION_SEARCH, query);
sf.setArguments(args);
getFragmentManager().beginTransaction()
.replace(R.id.fragmentContainer, sf).addToBackStack(null).commit();
If your activity is getting paused in between then also onCreate of your activity should not be called and that's where you are instantiating the fragment.i.e Fragment is not created again(view might be created again).
As as you have subscribed live data in onCreate of Fragment it should also not trigger an update(onChanged() won't be called for liveData) again.
Just to be sure about live data is not calling onChanged() again try below (i feel that's the culprit here as i can't see any other update happening)
As you will not want to send the same result to your search page again so distinctUntilChanged is a good check for your case.
viewModel.getSearchResults().distinctUntilChanged().observe(viewLifecycleOwner,
searchResultObserver)
Do subscription of live data in onActivityCreated of
fragment.(reference)
Instead of using globalScope you can use viewModelScope and launch from inside your ViewModel.(just a suggestion for clean code)
And what's SearchFragmentLifecycleObserver?
P.S - If you can share the ViewModel code and how the search callback's are triggering data it will be great.But Current lifecycle should not effect the creation of new fragment.
Use SaveStateHandle in your ViewModel to persist the loaded data, and don't use GlobalContext to do the fetching, encapsulate the fetching in VieModel. GlobalContext should only be used for fire and forget actions, which are not bound the any views or lifecycle.
How your SearchViewModel could look like:
#Parcelize
class SearchResult(
//fields ...
) : Parcelable
class SearchViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
private var isLoading : Boolean = false
fun searchLiveData() : LiveData<SearchResult> = savedStateHandle.getLiveData<SearchResult>(EXTRA_SEARCH)
fun fetchSearchResultIfNotLoaded() { //do this in onCreate
val liveData = savedStateHandle.getLiveData<SearchResult>(EXTRA_SEARCH)
if(liveData.value == null) {
if(isLoading)
return
isLoading = true
viewModelScope.launch {
try {
val result = withContext(Dispatchers.IO) {
//fetching task
SearchResult()
}
liveData.value = result
isLoading = false
}catch (e : Exception) {
//log
isLoading = false
}
}
}
}
companion object {
private const val EXTRA_SEARCH = "EXTRA_SEARCH"
}
}
And in your Search Fragment onCreate
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val searchResultObserver = Observer<Array<GoodreadsBook>> {
searchResultListViewAdapter.setData(it)
}
viewModel.searchLiveData().observe(viewLifeCycleScope, searchResultObserver)
viewModel.fetchSearchResultIfNotLoaded()
}
I think the Android team in charge of the documentation should really do a better job. I went ahead and just removed the SearchManager from the SearchViewand use the onQueryTextListener directly, only to see that with this approach I also get my listener called twice. But thanks to this post, I saw that apparently it's a bug with the emulator (or with the way SearchView handles the submit event). So if I press the OSK enter button everything works as expected.
Thanks everyone for their help!

Categories

Resources