I'm following the MVVM using ROOM,Retrofit,Koin DI etc. and inside of my MainFragment class i'm calling my bindUI() function that is responsible for fetching data through the viewModel asynchronously using kotlin coroutines as you can see below. Now, when i run my app it crashes almost immediately.
Here's what i tried: I placed a breakpoint inside of bindUI() and specifically on my first .await() call on val currentWeather and run the debugger. I noticed that as soon as the await call is resolved and the result is returned to the variable, the application crashes saying that Skipped 1501 frames! The application may be doing too much work on its main thread. and then Skipped 359 frames! The application may be doing too much work on its main thread.
Now, why would that be since i'm running those async calls inside of the Dispathcers.IO thread and at the moment of the crash i'm only executing one await() call?
Here's my MainFragment class:
const val UNIT_SYSTEM_KEY = "UNIT_SYSTEM"
class MainFragment(
private val weatherUnitConverter: WeatherUnitConverter
) : ScopedFragment() {
// Lazy inject the view model
private val viewModel: WeatherViewModel by viewModel()
private lateinit var unitSystem:String
private val TAG = MainFragment::class.java.simpleName
// View declarations
private lateinit var lcHourlyForecasts: LineChart
private lateinit var weeklyForecastRCV: RecyclerView
private lateinit var scrollView: NestedScrollView
private lateinit var detailsExpandedArrow:ImageView
private lateinit var detailsExpandedLayout: LinearLayout
private lateinit var dailyWeatherDetailsHeader:LinearLayout
private lateinit var settingsBtnImageView:ImageView
private lateinit var unitSystemImgView:ImageView
private lateinit var locationTxtView:TextView
// Current weather view declarations
private lateinit var currentWeatherDate:TextView
private lateinit var currentWeatherTemp:TextView
private lateinit var currentWeatherSummaryText:TextView
private lateinit var currentWeatherSummaryIcon:ImageView
private lateinit var currentWeatherPrecipProb:TextView
// Today/Details weather view declarations
private lateinit var todayHighLowTemp:TextView
private lateinit var todayWindSpeed:TextView
private lateinit var todayFeelsLike:TextView
private lateinit var todayUvIndex:TextView
private lateinit var todayPrecipProb:TextView
private lateinit var todayCloudCover:TextView
private lateinit var todayHumidity:TextView
private lateinit var todayPressure:TextView
private lateinit var todaySunriseTime:TextView
private lateinit var todaySunsetTime:TextView
// OnClickListener to handle the current weather's "Details" layout expansion/collapse
private val onCurrentWeatherDetailsClicked:View.OnClickListener = View.OnClickListener {
if(detailsExpandedLayout.visibility == View.GONE) {
detailsExpandedLayout.visibility = View.VISIBLE
detailsExpandedArrow.setImageResource(R.drawable.ic_arrow_up_black)
}
else {
detailsExpandedLayout.visibility = View.GONE
detailsExpandedArrow.setImageResource(R.drawable.ic_down_arrow)
}
}
// OnClickListener to allow navigating from this fragment to the settings one
private val onSettingsButtonClicked:View.OnClickListener = View.OnClickListener {
(activity as MainActivity).openSettingsPage()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.main_fragment, container, false)
// View initializations
scrollView = view.findViewById(R.id.nsv_main)
lcHourlyForecasts = view.findViewById(R.id.lc_hourly_forecasts)
detailsExpandedLayout = view.findViewById(R.id.ll_expandable)
detailsExpandedArrow = view.findViewById(R.id.iv_arrow)
dailyWeatherDetailsHeader = view.findViewById(R.id.current_weather_details_header)
dailyWeatherDetailsHeader.setOnClickListener(onCurrentWeatherDetailsClicked)
settingsBtnImageView = view.findViewById(R.id.settings)
settingsBtnImageView.setOnClickListener(onSettingsButtonClicked)
unitSystemImgView = view.findViewById(R.id.unitSystemImg)
locationTxtView = view.findViewById(R.id.location)
initCurrentWeatherViews(view)
initTodayWeatherViews(view)
// RCV initialization
weeklyForecastRCV = view.findViewById(R.id.weekly_forecast_rcv)
weeklyForecastRCV.adapter = WeeklyWeatherAdapter(listOf(),viewModel.preferences, this,weatherUnitConverter) // init the adapter with empty data
weeklyForecastRCV.setHasFixedSize(true)
// Disable nested scrolling to control the RCV scrolling via the parent NestedScrollView
weeklyForecastRCV.isNestedScrollingEnabled = false
return view
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
initLineChart()
bindUI()
}
private fun SharedPreferences.stringLiveData(key: String, defValue: String): SharedPreferenceLiveData<String> {
return SharedPreferenceStringLiveData(this, key, defValue)
}
private fun bindUI() = launch(Dispatchers.Main) {
//TODO:sp get the coordinates dynamically
viewModel.setLocCoordinates(37.8267,-122.4233)
// fetch current weather
val currentWeather = viewModel.currentWeatherData.await()
// fetch weekly weather
val weeklyWeather = viewModel.weeklyWeatherEntries.await()
// fetch the location
val weatherLocation = viewModel.weatherLocation.await()
// Observe the location for changes
weatherLocation.observe(viewLifecycleOwner, Observer { location ->
if(location == null) return#Observer
launch {
updateLocation(location)
}
})
// Observe the current weather live data
currentWeather.observe(viewLifecycleOwner, Observer {currently ->
if(currently == null) return#Observer
setCurrentWeatherDate(currently.time.toDouble())
// Observe the unit system sharedPrefs live data for changes
viewModel.preferences.stringLiveData(UNIT_SYSTEM_KEY, UnitSystem.SI.name.toLowerCase(Locale.ROOT))
.observe(viewLifecycleOwner, Observer {unitSystem ->
when(unitSystem) {
UnitSystem.SI.name.toLowerCase(Locale.ROOT) -> {
setCurrentWeatherTemp(currently.temperature)
setUnitSystemImgView(unitSystem)
}
UnitSystem.US.name.toLowerCase(Locale.ROOT) -> {
setCurrentWeatherTemp(weatherUnitConverter.convertToFahrenheit(
currently.temperature
))
setUnitSystemImgView(unitSystem)
}
}
})
setCurrentWeatherSummaryText(currently.summary)
setCurrentWeatherSummaryIcon(currently.icon)
setCurrentWeatherPrecipProb(currently.precipProbability)
})
// observe the weekly weather live data
weeklyWeather.observe(viewLifecycleOwner, Observer {weatherEntries ->
if(weatherEntries == null) return#Observer
// update the recyclerView with the new data
(weeklyForecastRCV.adapter as WeeklyWeatherAdapter).updateWeeklyWeatherData(weatherEntries)
initTodayData(weatherEntries[0])
})
}
/**
* Uses the location param's lat & longt values
* to determine the selected location and updates
* the view.
*/
private suspend fun updateLocation(location: WeatherLocation) {
withContext(Dispatchers.IO) {
val geocoder = Geocoder(activity,Locale.getDefault())
try {
val addr = geocoder.getFromLocation(location.latitude,location.longitude,1)
val adobj = addr[0]
locationTxtView.text = adobj.countryName
} catch (e:IOException) {
Log.d(TAG, e.printStackTrace().toString())
}
}
}
/**
* Initializes the views for the current weather.
*/
private fun initCurrentWeatherViews(view: View) {
currentWeatherDate = view.findViewById(R.id.current_weather_date)
currentWeatherTemp = view.findViewById(R.id.current_temp_main)
currentWeatherSummaryText = view.findViewById(R.id.current_weather_summary_text)
currentWeatherSummaryIcon = view.findViewById(R.id.current_weather_summary_icon)
currentWeatherPrecipProb = view.findViewById(R.id.current_weather_precip_text)
}
/**
* Initializes the views for the Detailed Today weather view.
*/
private fun initTodayWeatherViews(view: View?) {
if(view == null) return
todayHighLowTemp = view.findViewById(R.id.today_lowHighTemp)
todayWindSpeed = view.findViewById(R.id.today_windSpeed)
todayFeelsLike = view.findViewById(R.id.today_feelsLike)
todayUvIndex = view.findViewById(R.id.today_uvIndex)
todayPrecipProb = view.findViewById(R.id.today_precipProb)
todayCloudCover = view.findViewById(R.id.today_cloudCover)
todayHumidity = view.findViewById(R.id.today_humidity)
todayPressure = view.findViewById(R.id.today_pressure)
todaySunriseTime = view.findViewById(R.id.today_sunriseTime)
todaySunsetTime = view.findViewById(R.id.today_sunsetTime)
}
private fun setUnitSystemImgView(unitSystem:String) {
val resource = when(unitSystem) {
UnitSystem.SI.name.toLowerCase(Locale.ROOT)
-> R.drawable.ic_celsius
UnitSystem.US.name.toLowerCase(Locale.ROOT)
-> R.drawable.ic_fahrenheit
else -> R.drawable.ic_celsius
}
unitSystemImgView.setImageResource(resource)
}
/**
* Links the data to the view for the Today(Details) Weather View.
*/
private fun initTodayData(weekDayWeatherEntry: WeekDayWeatherEntry) {
// Observe the unit system sharedPrefs live data for changes
viewModel.preferences.stringLiveData(UNIT_SYSTEM_KEY, UnitSystem.SI.name.toLowerCase(Locale.ROOT))
.observe(viewLifecycleOwner, Observer {unitSystem ->
when(unitSystem) {
UnitSystem.SI.name.toLowerCase(Locale.ROOT) -> {
setTodayWeatherLowHighTemp(weekDayWeatherEntry.temperatureLow,weekDayWeatherEntry.temperatureHigh)
setTodayWeatherWindSpeed(weekDayWeatherEntry.windSpeed,unitSystem)
setTodayWeatherFeelsLike(weekDayWeatherEntry.apparentTemperatureLow,weekDayWeatherEntry.apparentTemperatureHigh)
}
UnitSystem.US.name.toLowerCase(Locale.ROOT) -> {
setTodayWeatherLowHighTemp(weatherUnitConverter.convertToFahrenheit(
weekDayWeatherEntry.temperatureLow),
weatherUnitConverter.convertToFahrenheit(
weekDayWeatherEntry.temperatureHigh))
setTodayWeatherWindSpeed(weatherUnitConverter.convertToMiles(weekDayWeatherEntry.windSpeed),unitSystem)
setTodayWeatherFeelsLike(weatherUnitConverter.convertToFahrenheit(
weekDayWeatherEntry.apparentTemperatureLow)
,weatherUnitConverter.convertToFahrenheit(weekDayWeatherEntry.apparentTemperatureHigh))
}
}
})
setTodayWeatherUvIndex(weekDayWeatherEntry.uvIndex)
setTodayWeatherPrecipProb(weekDayWeatherEntry.precipProbability)
setTodayWeatherCloudCover(weekDayWeatherEntry.cloudCover)
setTodayWeatherHumidity(weekDayWeatherEntry.humidity)
setTodayWeatherPressure(weekDayWeatherEntry.pressure)
setTodayWeatherSunriseTime(weekDayWeatherEntry.sunriseTime)
setTodayWeatherSunsetTime(weekDayWeatherEntry.sunsetTime)
}
...
}
WeatherViewModel.kt:
class WeatherViewModel(
private val forecastRepository: ForecastRepository,
context:Context
) : ViewModel() {
private var mLatitude:Double = 0.0
private var mLongitute:Double = 0.0
private val appContext = context.applicationContext
// Retrieve the sharedPrefs
val preferences:SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(appContext)
// This will run only when currentWeatherData is called from the View
val currentWeatherData by lazyDeferred {
forecastRepository.getCurrentWeather(mLatitude, mLongitute)
}
val weeklyWeatherEntries by lazyDeferred {
val currentDateEpoch = LocalDate.now().toEpochDay()
forecastRepository.getWeekDayWeatherList(mLatitude, mLongitute, currentDateEpoch)
}
val weatherLocation by lazyDeferred {
forecastRepository.getWeatherLocation()
}
fun setLocCoordinates(latitude:Double,longitude:Double) {
mLatitude = latitude
mLongitute = longitude
}
}
Here's my custom Lazy<Deferred<T>> fun inside of my Delegates.kt file:
fun<T> lazyDeferred(block: suspend CoroutineScope.() -> T) : Lazy<Deferred<T>> {
return lazy {
GlobalScope.async(start = CoroutineStart.LAZY) {
block.invoke(this)
}
}
}
Here's my repository class just in case:
private const val WEEKLY_FORECAST_DAYS_COUNT = 7
/**
* The Repository class responsible
* for caching the downloaded weather data
* and for swapping between different data sources.
*/
class ForecastRepositoryImpl(
private val currentWeatherDao: CurrentWeatherDao,
private val weekDayWeatherDao: WeekDayWeatherDao,
private val weatherLocationDao: WeatherLocationDao,
private val locationProvider: LocationProvider,
private val weatherNetworkDataSource: WeatherNetworkDataSource
) : ForecastRepository {
init {
weatherNetworkDataSource.apply {
// Persist downloaded data
downloadedCurrentWeatherData.observeForever { newCurrentWeather: CurrentWeatherResponse? ->
persistFetchedCurrentWeather(newCurrentWeather!!)
}
downloadedWeeklyWeatherData.observeForever { newWeeklyWeather: WeeklyWeatherResponse? ->
persistFetchedWeeklyWeather(newWeeklyWeather!!)
}
}
}
override suspend fun getCurrentWeather(latitude:Double,longitude:Double): LiveData<CurrentWeatherEntry> {
return withContext(Dispatchers.IO) {
initWeatherData(latitude,longitude)
return#withContext currentWeatherDao.getCurrentWeather()
}
}
override suspend fun getWeekDayWeatherList(latitude: Double,longitude: Double,time:Long): LiveData<out List<WeekDayWeatherEntry>> {
return withContext(Dispatchers.IO) {
initWeatherData(latitude,longitude)
return#withContext weekDayWeatherDao.getFutureWeather(time)
}
}
override suspend fun getWeatherLocation(): LiveData<WeatherLocation> {
return withContext(Dispatchers.IO) {
return#withContext weatherLocationDao.getWeatherLocation()
}
}
private suspend fun initWeatherData(latitude:Double,longitude:Double) {
// retrieve the last weather location from room
val lastWeatherLocation = weatherLocationDao.getWeatherLocation().value
if(lastWeatherLocation == null ||
locationProvider.hasLocationChanged(lastWeatherLocation)) { // then this is the first time we are launching the app
fetchCurrentWeather()
fetchWeeklyWeather()
return
}
if(isFetchCurrentNeeded(lastWeatherLocation.zonedDateTime))
fetchCurrentWeather()
if(isFetchWeeklyNeeded())
fetchWeeklyWeather()
}
/**
* Checks if the current weather data should be re-fetched.
* #param lastFetchedTime The time at which the current weather data were last fetched
* #return True or false respectively
*/
private fun isFetchCurrentNeeded(lastFetchedTime: ZonedDateTime) : Boolean {
val thirtyMinutesAgo = ZonedDateTime.now().minusMinutes(30)
return lastFetchedTime.isBefore(thirtyMinutesAgo)
}
/**
* Fetches the Current Weather data from the WeatherNetworkDataSource.
*/
private suspend fun fetchCurrentWeather() {
weatherNetworkDataSource.fetchCurrentWeather(
locationProvider.getPreferredLocationLat(),
locationProvider.getPreferredLocationLong()
)
}
private fun isFetchWeeklyNeeded(): Boolean {
val todayEpochTime = LocalDate.now().toEpochDay()
val futureWeekDayCount = weekDayWeatherDao.countFutureWeekDays(todayEpochTime)
return futureWeekDayCount < WEEKLY_FORECAST_DAYS_COUNT
}
private suspend fun fetchWeeklyWeather() {
weatherNetworkDataSource.fetchWeeklyWeather(
locationProvider.getPreferredLocationLat(),
locationProvider.getPreferredLocationLong()
)
}
/**
* Caches the downloaded current weather data to the local
* database.
* #param fetchedCurrentWeather The most recently fetched current weather data
*/
private fun persistFetchedCurrentWeather(fetchedCurrentWeather:CurrentWeatherResponse) {
// Using a GlobalScope since a Repository class doesn't have a lifecycle
GlobalScope.launch(Dispatchers.IO) {
// cache the data
currentWeatherDao.upsert(fetchedCurrentWeather.currentWeatherEntry)
weatherLocationDao.upsert(fetchedCurrentWeather.location)
}
}
/**
* Caches the downloaded weekly weather data to the local
* database.
* #param fetchedWeeklyWeather The most recently fetched weekly weather data
*/
private fun persistFetchedWeeklyWeather(fetchedWeeklyWeather: WeeklyWeatherResponse) {
fun deleteOldData() {
val time = LocalDate.now().toEpochDay()
weekDayWeatherDao.deleteOldEntries(time)
}
GlobalScope.launch(Dispatchers.IO) {
deleteOldData()
val weekDayEntriesList = fetchedWeeklyWeather.weeklyWeatherContainer.weekDayEntries
weekDayWeatherDao.insert(weekDayEntriesList)
}
}
}
EDIT: Here's the crash log i found out about earlier today:
2020-04-13 01:43:48.628 26875-26904/com.nesoinode.flogaweather E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-2
Process: com.nesoinode.flogaweather, PID: 26875
java.lang.NullPointerException: Attempt to invoke virtual method 'int com.nesoinode.flogaweather.model.db.entity.WeatherLocation.getId()' on a null object reference
at com.nesoinode.flogaweather.model.db.dao.WeatherLocationDao_Impl$1.bind(WeatherLocationDao_Impl.java:34)
at com.nesoinode.flogaweather.model.db.dao.WeatherLocationDao_Impl$1.bind(WeatherLocationDao_Impl.java:26)
at androidx.room.EntityInsertionAdapter.insert(EntityInsertionAdapter.java:63)
at com.nesoinode.flogaweather.model.db.dao.WeatherLocationDao_Impl.upsert(WeatherLocationDao_Impl.java:52)
at com.nesoinode.flogaweather.model.repository.ForecastRepositoryImpl$persistFetchedCurrentWeather$1.invokeSuspend(ForecastRepositoryImpl.kt:131)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:561)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:727)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:667)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:655)
It's pointing to this part of my repository class as the root cause. I can't tell why though.
private fun persistFetchedCurrentWeather(fetchedCurrentWeather:CurrentWeatherResponse) {
// Using a GlobalScope since a Repository class doesn't have a lifecycle
GlobalScope.launch(Dispatchers.IO) {
// cache the data
currentWeatherDao.upsert(fetchedCurrentWeather.currentWeatherEntry)
weatherLocationDao.upsert(fetchedCurrentWeather.location)
}
}
UPDATE #2:
CurrentWeatherEntry :
const val CURRENT_WEATHER_ID = 0
#Entity(tableName = "current_weather")
data class CurrentWeatherEntry(
val time: Long, // epoch timestamp
val icon: String,
val summary: String,
val precipProbability: Double,
val temperature: Double
) {
#PrimaryKey(autoGenerate = false)
var id:Int = CURRENT_WEATHER_ID
}
WeatherLocation:
const val WEATHER_LOCATION_ID = 0
#Entity(tableName = "weather_location")
data class WeatherLocation(
val latitude: Double,
val longitude: Double,
val timezone: String
) {
#PrimaryKey(autoGenerate = false)
var id:Int = WEATHER_LOCATION_ID
private var epochTimeVal:Long = 0
val zonedDateTime:ZonedDateTime
get() {
val instant = Instant.ofEpochMilli(this.epochTimeVal)
val zoneId = ZoneId.of(timezone)
return ZonedDateTime.ofInstant(instant,zoneId)
}
fun setEpochTimeVal(time:Long) {
this.epochTimeVal = time}
fun getEpochTimeVal() : Long = epochTimeVal
}
and CurrentWeatherResponse:
data class CurrentWeatherResponse(
// Tells GSON that the "currently" field of the JSON returned by the
// API should be tied with our CurrentWeatherEntry data class
#SerializedName("currently")
val currentWeatherEntry: CurrentWeatherEntry,
#Embedded
val location: WeatherLocation
) {
init {
location.setEpochTimeVal(currentWeatherEntry.time)
}
}
According to the further diagnosis you made, the problem is unrelated to lazyDeferred, blocking the main thread, or coroutines in general. Your CurrentWeatherDao sometimes returns a CurrentWeatherResponse with location == null.
You never specified where forecastRepository.getWeatherLocation() should be executed so it gets executed on the dispatcher of your bindUI function which is Dispatchers.Main.
This means the request blocks your UI thread and causes the warning you're seeing in the log.
You need to specify that it executes on a separate dispatcher so the UI can continue updating normally:
lazyDeferred {
withContext(Dispatchers.IO) {
forecastRepository.getWeatherLocation()
}
}
As a separate issue, your lazyDeferred is slightly redundant in that it is "double"-lazy. You could remove the outer Lazy<T> and it would still work exactly the same way, or remove the start = CoroutineStart.LAZY and have the result arrive slightly sooner. (This essentially depends on whether the request starts when the Lazy is resolved, or when Deferred.await is called)
Related
I'm new here and I'm new to programming in general.
I followed a YouTube video to make a simple todo list app for android and have since modified it into a visitors register.
I am copying people who sign in to an external txt file (that part is ok because I can grab the information as it's entered) but I have been struggling for about a week now to try to get the names of people who are signing out.
Basically a name and date/time get written to myExternalFile on btnAddTodo
is it possible in the btnDeleteDoneTodo to grab the names from deleteDoneTodos() and add them to myEternalFile??
class MainActivity : AppCompatActivity() {
private lateinit var todoAdapter: TodoAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
todoAdapter = TodoAdapter(mutableListOf())
rvTOdoItems.adapter = todoAdapter
rvTOdoItems.layoutManager = LinearLayoutManager(this)
val fileName = "log.txt"
val filePath = "Sign In\\"
val myExternalFile = File (getExternalFilesDir(filePath),fileName)
btnAddTodo.setOnClickListener {
val todoTitle = etTodoTitle.text.toString().uppercase()
val regTitle = etRegTitle.text.toString().uppercase()
if (todoTitle.isNotEmpty() && regTitle.isNotEmpty()) {
val todo = Todo(todoTitle+"\n"+regTitle)
myExternalFile.appendText(todoTitle +" "+ regTitle +" "+ todoAdapter.getCurrentDateTime() +"\n")
todoAdapter.addTodo(todo)
etTodoTitle.text.clear()
etRegTitle.text.clear()
}
}
btnDeleteDoneTodo.setOnClickListener {
todoAdapter.deleteDoneTodos()
}
}
class Todo (
val title : String,
var isChecked: Boolean = false
class TodoAdapter(
val todos: MutableList<Todo>
) : RecyclerView.Adapter<TodoAdapter.TodoViewHolder>() {
class TodoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
return TodoViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.todo,
parent,
false
)
)
}
fun getCurrentDateTime() : Date {
return Calendar.getInstance().time
}
fun addTodo (todo : Todo) {
todos.add(todo)
notifyItemInserted(todos.size-1)
}
fun deleteDoneTodos () {
todos.removeAll(Todo::isChecked)
notifyDataSetChanged()
}
private fun toggleStrikeThrough(tvtodoTitle: TextView, isChecked: Boolean) {
if (isChecked) {
tvtodoTitle.paintFlags = tvtodoTitle.paintFlags or STRIKE_THRU_TEXT_FLAG
} else {
tvtodoTitle.paintFlags = tvtodoTitle.paintFlags and STRIKE_THRU_TEXT_FLAG.inv()
}
}
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
val curTodo = todos[position]
holder.itemView.apply {
tvTodoTitle.text = curTodo.title
cdDone.isChecked = curTodo.isChecked
toggleStrikeThrough(tvTodoTitle, curTodo.isChecked)
cdDone.setOnCheckedChangeListener { _, isChecked ->
toggleStrikeThrough(tvTodoTitle, isChecked)
curTodo.isChecked = !curTodo.isChecked
}
}
}
override fun getItemCount(): Int {
return todos.size
}
}
I have tried appending just about every variable I think would be relevant with .toString() in the btnDeleteDoneTodos method, I have tried making separate lists in the deleteDoneTodos function, I tried making a new function that outputs a list but I have failed.
I don't know if it's even possible with the way it has all been implemented - like I said I'm very new to programming so any help would be appreciated.
Thanks
edit.
I have managed to grab a single person leaving by adding the following leavers function to the todoAdapter class:
fun leavers (tvtodoTitle: TextView, isChecked: Boolean) : String {
var going = ""
if (isChecked) going = tvtodoTitle.text.toString()
return going
}
which returns a single string (hence only one person) that I can then append to my external file. I have tried to change the function to mutable list then iterate through it in btnDeleteDoneTodo listener but it will still only return one name.
I think I'm going to can this and go with Dewerros suggestion and try to setup a local Db, if anybody does have a solution it would still be gladly received as this has annoyed me for far too long now.
class day_1 : Fragment() {
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
private var _binding: FragmentDay1Binding? = null
private val binding get() = _binding!!
private var mediaPlayer: MediaPlayer? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
binding.startbutton.setOnClickListener {
if (mediaPlayer == null) {
mediaPlayer = MediaPlayer.create(activity, R.raw.days)
}
mediaPlayer?.start()
}
binding.pausebutton.setOnClickListener {
if (mediaPlayer?.isPlaying == true) {
mediaPlayer?.pause()
} else {
mediaPlayer?.start()
}
}
binding.stopbutton.setOnClickListener {
mediaPlayer?.stop()
mediaPlayer = null
}
fun onStop() {
super.onStop()
mediaPlayer?.release()
mediaPlayer = null
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentDay1Binding.inflate(inflater,container,false)
return binding.root
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* #param param1 Parameter 1.
* #param param2 Parameter 2.
* #return A new instance of fragment day_1.
*/
// TODO: Rename and change types and number of parameters
#JvmStatic
fun newInstance(param1: String, param2: String) =
day_1().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
what is above is my code. If I click the button, application was shutting down. First, I think that Media Player is problem. but application without setOnClickListener has no problem. Please help me masters.... Is it problem that setOnClickListener is in Fragment?
And Can I amend this code? or I restart project? Perhaps If you know why this problem exist, Please tell me resolution...
You can take a look for fragment lifecycles at here https://developer.android.com/guide/fragments/lifecycle
You are setting on click listener at onCreate method, in fragments views are created at onCreateView method so you are trying to access your view before it is created. So instead of onCreate method you need to set your click listener after your binding/view is ready, for example you can set your click listener before returning binding.root at onCreateView. Also you can set your click listener at onViewCreated method.
I am using a recyclerView to show my listItems in the home fragment. But I have been stuck on how to open a different activity when items are clicked. How can I do for the next steps? Could you please help me to solve the issue? I would appreciate the help.
HomeFragment.kt
class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
lateinit var recycle1:RecyclerView
private val list = ArrayList<Locations>()
private val adapter:Adapter = Adapter(list)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
var v =inflater.inflate(R.layout.fragment_home, container, false)
recycle1 = v.findViewById(R.id.rv_item)
recycle1.layoutManager = LinearLayoutManager(activity)
list.clear()
testData()
val adapterr = Adapter(list)
recycle1.adapter = adapterr
adapter.notifyDataSetChanged()
recycle1.setHasFixedSize(true)
return v
}
private fun testData(){
list.add(Locations(R.drawable.book_cafe,"The Book Cafe","20 Martin Rd","Cafe"))
list.add(Locations(R.drawable.citysprouts,"City Sprouts","102 Henderson Road","Cafe"))
list.add(Locations(R.drawable.esplanade,"Library#esplande","8 Raffles Ave","Library"))
list.add(Locations(R.drawable.hf,"Library#Harbourfront","1 HarbourFront Walk","Library"))
list.add(Locations(R.drawable.mangawork,"MangaWork","291 Serangoon Rd","Cafe"))
list.add(Locations(R.drawable.orchard,"Library#orchard","277 Orchard Road","Library"))
list.add(Locations(R.drawable.rabbitandfox,"Rabbit&Fox","160 Orchard Rd","Cafe"))
list.add(Locations(R.drawable.sixlettercoffee,"T6 Letter Coffee","259 Tanjong Katong Rd","Cafe"))
}
}
Adapter.kt
class Adapter (val listItem:ArrayList<Locations>) : RecyclerView.Adapter<Adapter.RecycleViewHolder>(){
inner class RecycleViewHolder(itemView: View):RecyclerView.ViewHolder(itemView){
val itemImage: ShapeableImageView = itemView.findViewById(R.id.item_image)
val heading: TextView = itemView.findViewById(R.id.item_title)
val detail: TextView = itemView.findViewById(R.id.item_detail)
val category: TextView = itemView.findViewById(R.id.item_categories)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecycleViewHolder {
val view:View = LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false)
return RecycleViewHolder(view)
}
override fun getItemCount(): Int {
return listItem.size
}
override fun onBindViewHolder(holder: RecycleViewHolder, position: Int) {
val item = listItem[position]
holder.itemImage.setImageResource(item.itemImage)
holder.heading.text = item.headings
holder.detail.text = item.detail
holder.category.text = item.category
}
Locations. kt
data class Locations(var itemImage:Int,var headings :String,var detail :String,var category :String)
You have to take help of an interface to navigate :
First Create an interface , Suppose name it Navigate
interface Navigate {
fun onRecyclerViewItemClicked(location : Location)
}
Then you have to make use of this interface in the adapter as well as the fragment :
In your adapter , you have to do the following :
class Adapter (val listItem:ArrayList<Locations>) : RecyclerView.Adapter<Adapter.RecycleViewHolder>(){
//Declare listener object
var listener: Navigate? = null
inner class RecycleViewHolder(itemView: View):RecyclerView.ViewHolder(itemView){
val itemImage: ShapeableImageView = itemView.findViewById(R.id.item_image)
val heading: TextView = itemView.findViewById(R.id.item_title)
val detail: TextView = itemView.findViewById(R.id.item_detail)
val category: TextView = itemView.findViewById(R.id.item_categories)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecycleViewHolder {
val view:View = LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false)
return RecycleViewHolder(view)
}
override fun getItemCount(): Int {
return listItem.size
}
override fun onBindViewHolder(holder: RecycleViewHolder, position: Int) {
val item = listItem[position]
holder.itemImage.setImageResource(item.itemImage)
holder.heading.text = item.headings
holder.detail.text = item.detail
holder.category.text = item.category
// Now suppose you want to navigate to another fragment / Activity on click of the itemImage , then do the following
holder.itemImage.setOnClickListener{
listener?.onRecyclerViewItemClicked(item)
}
}
Now in your fragment extend the Navigate Class and implement the override the methods :
class HomeFragment : Fragment() , Navigate {
// Rest of your code
override fun onRecyclerViewItemClicked(location : Location){
// The argument location here is the information of the item that is clicked of
// the RecyclerView
// Here write your code to navigate to the next fragment / activity based on what
//you are using NavController / Fragment Manager
}
}
Set a clickListener in init {} block of your view holder
For eg.
inner class RecycleViewHolder(itemView: View):RecyclerView.ViewHolder(itemView){
val itemImage: ShapeableImageView = itemView.findViewById(R.id.item_image)
val heading: TextView = itemView.findViewById(R.id.item_title)
val detail: TextView = itemView.findViewById(R.id.item_detail)
val category: TextView = itemView.findViewById(R.id.item_categories)
init {
itemView.setOnClickListener { // start your activity using view context }
}
}
Never set your click listener in onBindViewHolder() method as click listener will set multiple times because this method method is called every time items are bound in recycler view.
Reference official doc
I want to make my recyclerview paginationscrolling using retrofit.
I already complete get Json data using retrofit. It means interface API is correct from API Document.
However, if I loaded more 20items. can not scroll more items in Client.
when I checked Server data. per one page can get maximum 20items.
For example, if I loaded 25items in my recyclerview.
page 0: 20, page 1: 5.
if I want scrolling all items, How can I make paginationscrolling for retrofit??
check some my code and help me..
Response
Interface API
#GET("/store/cart/mine")
fun getCart(#Header("Authorization") token: String?, #Query("page") page:Int): Call<CartResponse>
CartViewActivity
class CartViewActivity : AppCompatActivity(), SwipeRefreshLayout.OnRefreshListener {
private val lastVisibleItemPosition: Int
get()= LinearLayoutManager.HORIZONTAL
private lateinit var scrollListener: RecyclerView.OnScrollListener
lateinit var mAdapter: CartItemRecyclerAdapter
#RequiresApi(Build.VERSION_CODES.N)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_cart_view)
val page = 0
val token = SharedPreference.getTokenInfo(this)
Client.retrofitService.getCart(token,page).enqueue(object :Callback<CartResponse> {
override fun onResponse(call: Call<CartResponse>, response: Response<CartResponse>) {
if (response?.isSuccessful == true) {
swipeRefreshLo.setOnRefreshListener(this#CartViewActivity)
showdata(response.body()?.docs!!)
}else if(response?.isSuccessful==false) {
val er = Gson().fromJson(response.errorBody()?.charStream(), ErrorResponse::class.java)
if (er.code==60202) {
}
}
}
override fun onFailure(call: Call<CartResponse>, t: Throwable) {
}
})
}
private fun showdata(results: MutableList<cartDocs>) {
recycler_view.apply {
mAdapter=CartItemRecyclerAdapter(context,context as Activity, results)
recycler_view.adapter=mAdapter
recycler_view.layoutManager=LinearLayoutManager(context)
setRecyclerViewScrollListener()
}
}
private fun setRecyclerViewScrollListener() {
scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
val totalItemCount = recyclerView.layoutManager!!.itemCount
if(totalItemCount == lastVisibleItemPosition + 1) {
Log.d("MyTAG", "Load new list")
recyclerView.removeOnScrollListener(scrollListener)
}
}
}
}
override fun onRefresh() {
swipeRefreshLo.isRefreshing = false
}
}
Firstly, you need to modify your adapter to be able to update existing data, instead of creating new adapter every time you fetch new data.
Adapter should be initialised before getting the data, to be able to call methods on it.
You should create a method inside the adapter, something like
fun updateData(results: MutableList<cartDocs>) {
dataSet.addAll(results)
notifyItemRangeInserted(start, newItemsSize)
}
Then, we you get response from server in onSuccess() you should call method above, and increment page, so the next time you load data, you get new items.
Data should be fetched first time when screen loads (page will be 0), and then when user scrolls to bottom of RV ().
I'm trying to get a info box working with Google Maps API. When trying to run the application, and adding a marker with custom info box it crashes.
Here is my code:
class MainMapsActivity : AppCompatActivity(), OnMapReadyCallback {
private lateinit var mMap: GoogleMap
private val test: ArrayList<String> = arrayListOf()
private var mLocationPermissionGranted: Boolean = false
private val PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION: Int = 1234
// ^Number isnt definitive, as long as its unique inside the application
private val DEFAULT_ZOOM: Float = 15.0F
private var mLastKnownLocation: Location? = null
private val mDefaultLocation = LatLng(60.312491, 24.484248)
private lateinit var mFusedLocationProviderClient: FusedLocationProviderClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_maps)
// Obtain the SupportMapFragment and get notified when the map is ready to be used.
val mapFragment = supportFragmentManager
.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this)
}
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
getDeviceLocation()
//PRESSING WILL ADD MARKER
mMap.setOnMapClickListener(GoogleMap.OnMapClickListener { point ->
val builder: AlertDialog.Builder
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder = AlertDialog.Builder(this, android.R.style.Theme_Material_Dialog_Alert)
} else {
builder = AlertDialog.Builder(this)
}
val marker = MarkerOptions().position(point)
builder.setTitle("Are you sure you want to add a map location here?")
.setMessage("Are you sure you want to add a map location here?")
.setPositiveButton(android.R.string.yes, DialogInterface.OnClickListener { dialog, which ->
mMap.addMarker(marker)
//CUSTOM MARKER
.setIcon(BitmapDescriptorFactory.fromResource(R.mipmap.pinetree_foreground))
})
.setNegativeButton(android.R.string.no, DialogInterface.OnClickListener { dialog, which ->
// do nothing
})
.show()
true
val mapInfoWindowFragment = supportFragmentManager.findFragmentById(R.id.infoWindowMap) as MapInfoWindowFragment
//Set Custom InfoWindow
val infoWindow = InfoWindow(point, InfoWindow.MarkerSpecification(0, 0), mapInfoWindowFragment)
// Shows the InfoWindow or hides it if it is already opened.
mapInfoWindowFragment.infoWindowManager()?.toggle(infoWindow, true);
})
And here is the error Logcat gives me:
kotlin.TypeCastException: null cannot be cast to non-null type com.example.sampo.luontoalueet.MapInfoWindowFragment
at com.example.sampo.luontoalueet.MainMapsActivity$onMapReady$1.onMapClick(MainMapsActivity.kt:123)
My question is how to change change this statement into non-null type?
val mapInfoWindowFragment = supportFragmentManager.findFragmentById(R.id.infoWindowMap) as MapInfoWindowFragment
Your findfragmentById is returning null (which indicates that your fragment does not exist).
If you're absolutely sure that fragment cannot be null at this point, you need to review your code. Maybe your fragment is not attached to supportFragmentManager?
If you want to handle a case where this fragment might not exist, You can use nullable cast as?:
val mapInfoWindowFragment = supportFragmentManager.findFragmentById(R.id.infoWindowMap) as? MapInfoWindowFragment
Then you can check conditionally if(mapInfoWindowFragment == null) and handle the null case.