I would like to sort my items in order to put at the beginning chosen elements, I created a small piece of code allowing it which works rather well, but when I update my array in the adapter, the recyclerview is unchanged. I tried to do it with an update function in my adapter, and with the debugger, I see that the passed array is modified but nothing in the view... I also tried to send my array when creating the adapter but nothing changes. More weird, because I also have a filter by alphabetical order which works well.... Anyone have an idea? Thanks
Here's is my sort code which work, I tested it with the debugger:
for (i in 0 until folders.size) {
val isRead: String? = prefNewDir.getString(folders[i].absolutePath, null)
if (isRead != null) {
println("before : "+folders[i] + "et i :" + i)
val old= folders[i]
folders.removeAt(i)
folders.add(0, old)
println("after : "+folders[i] + "et i :" + i)
}
}
Here my update method in my adapter:
fun updateData(data: MutableList<File>) {
println("update before : "+items[0])
items = data
println("update after : "+items[0])
notifyDataSetChanged()
}
Here's my adapter code:
class DirAdapter(
private val context: Context,
private var items: MutableList<File>,
) : RecyclerView.Adapter<DirAdapter.DirViewHolder>() {
private val prefNewDir: SharedPreferences = context.getSharedPreferences("new_files", Context.MODE_PRIVATE)
class DirViewHolder(view : View) : RecyclerView.ViewHolder(view) {
val Dir_name = view.findViewById<TextView>(R.id.file_name)
val DirIsRead = view.findViewById<ImageView>(R.id.file_new)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType : Int):DirViewHolder{
val view : View = LayoutInflater.from(parent.context) //Mode dossier
.inflate(R.layout.folder_element, parent, false)
return DirViewHolder(view)
}
fun updateData(data: MutableList<File>) {
println("update data : "+items[0])
items = data
println("update data : "+items[0])
notifyDataSetChanged()
}
override fun onBindViewHolder(holder: DirViewHolder, position: Int) {
val currentDir = items[position]
holder.Dir_name.text = currentDir.name
val isRead: String? = prefNewDir.getString(currentDir.absolutePath, null) // On recupere si oui ou non le fichier est lu
if (isRead!=null) {
holder.DirIsRead.visibility = View.VISIBLE
} else
holder.DirIsRead.visibility = View.GONE
holder.itemView.setOnClickListener {
val intent = Intent(context, FilesActivity::class.java)
intent.putExtra("path",currentDir.absolutePath)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
}
holder.itemView.setOnLongClickListener {
val popupMenu = PopupMenu(context, it)
if (isRead != null) { //Popup seulement sur un element
popupMenu.menu.add("Marquer comme lu")
}
popupMenu.setOnMenuItemClickListener {
if (it.title == "Marquer comme lu") {
putFolderAsRead(currentDir)
holder.DirIsRead.visibility = View.GONE
}
true
}
popupMenu.show()
true
}
}
private fun putFolderAsRead(folder: File) {
val editor: SharedPreferences.Editor = prefNewDir.edit()
editor.remove(folder.absolutePath)
//Dossier courant
val folderList = folder.listFiles()
for (dListItem in folderList) {
if(dListItem.isDirectory) //Si un dossier est present on reboucle dessus
putFolderAsRead(dListItem)
editor.remove(dListItem.absolutePath)
println(dListItem.absolutePath + " Marque comme lu !")
}
editor.apply()
}
override fun getItemCount() = items.size
}
First of all for storing data in shared preferences, I'd prefer you to use my library here. You can refer the docs to see how it works. Also, I'd prefer you to use a data class instead of the File class. That won't give you what you want. Since, you already show the files without sort, you just need some modifications:
Make a data class like this:
data class MyFile(var file: File, var isNew: Boolean = false)
Now, the sorting part will be like this:
var folders = getFolders(Environment.getExternalStorageDirectory())
for (i in 0 until folders.size) {
val isRead: String? = prefNewDir.getString(folders[i].absolutePath, null)
folders.get(i).isNew = if(isRead == null) false else true
}
Now also, you might want to get the folder without sorting and you might be a bit confused, so, here is the code for getting the folders
fun getFolders(location:File) : ArrayList<MyFile>(){
val files = location.listFile()
var foldersList = ArrayList<MyFile>()
for (file in files) {
if(file.isDirectory){ foldersList.add(MyFile(file)) }
}
}
This will do the task for you.
Related
I am trying to add multiple GeoJsonLayer. When user click on 1 polygon I would like to display data of clicked polygon. Those are some of my polygons:
This is my function in which I get data from API and draw geojson layers on google maps.
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
var geojson = ArrayList<GeojsonResponse>()
val userGerkId: String? = SharedPrefManager.getInstance(applicationContext).user.gerkMID
RetrofitClientLands.instance.getLand(userGerkId).enqueue(object : Callback<GeojsonResponse> {
override fun onResponse(
call: Call<GeojsonResponse>,
response: Response<GeojsonResponse>
) {
if (response.code() == 200) {
val body = response.body()
if (body != null) {
for (i in 0 until body.lands.size) {
val geo = body.lands[i]
val geos = geo.get("geometry")
val properties = geo.get("properties")
//Log.i("Properties", properties.toString())
val geometryJson: JSONObject = JSONObject(geos.toString())
val geoJsonData: JSONObject = geometryJson
val layer = GeoJsonLayer(mMap, geoJsonData)
val style: GeoJsonPolygonStyle = layer.defaultPolygonStyle
style.fillColor = resources.getColor(R.color.darkGray)
style.strokeColor = resources.getColor(R.color.darkerGray)
style.strokeWidth = 2f
layer.addLayerToMap()
layer.setOnFeatureClickListener(
GeoJsonOnFeatureClickListener { feature: Feature ->
Toast.makeText(
applicationContext,
"GeoJSON polygon clicked: $properties",
Toast.LENGTH_SHORT
).show()
})
}
} else {
Log.i("Map-error", response.errorBody().toString())
}
}
}
override fun onFailure(call: Call<GeojsonResponse>, t: Throwable) {
Log.i("Map response", t.message.toString())
Toast.makeText(
applicationContext,
"Prišlo je do napake, na novo zaženite aplikacijo",
Toast.LENGTH_LONG
).show()
}
})
// adding marker
mMap.moveCamera(CameraUpdateFactory.newLatLng(LatLng(45.92757404830929, 15.595209429220395)))
mMap.uiSettings.isZoomControlsEnabled = true
mMap.animateCamera( CameraUpdateFactory.zoomTo( 12.5f ) );
}
I tried to set style.isClickable = false and then add code below, but every time i clicked on layer, it returns the same data (because whole map is clickable ig).
mMap.setOnMapClickListener {
Log.i("Map_clicked", "polygon: $properties")
}
So is there any other way of doing this? This thread has the same problem described.
How to add multiple GeoJsonLayer runtime and get click event in android
I would like to display suggestions in recyclerView instead of embedded UI of the searchView (like on picture below)
Suggestions are loaded from API call on each text query change of my searchView.
Then, each time I receipt new suggestions, I'm executing an AsyncTask which populate the UI suggestions using Cursor
class FetchSearchUserTermSuggestionsTask(var searchview: SearchView) : AsyncTask<String?, Void?, Cursor>() {
override fun doInBackground(vararg params: String?): Cursor {
val cursor =
MatrixCursor(columnNamesArray)
var list : List<UserSuggestionDto>? = null
var call = RetrofitClient.userService.suggestionForTextSearch(0, 50, params[0]!!)
val response = call.execute()
if (response.isSuccessful) {
list = response.body()!!
} else {
//todo : throw exception
Log.e(EventAddFragment.TAG, response.message())
Log.e(EventAddFragment.TAG, response.errorBody().toString())
}
// parse your search terms into the MatrixCursor
if(list != null) {
for (index in list.indices) {
val term = list[index].fullname
val image = list[index].profilePic
val id = list[index].id
val row = arrayOf(index, id, image, term)
cursor.addRow(row)
}
}
return cursor
}
override fun onPostExecute(result: Cursor) {
searchview.suggestionsAdapter.changeCursor(result)
}
companion object {
private val columnNamesArray = arrayOf(BaseColumns._ID,
UserSuggestionDto.SUGGEST_COLUMN_ID,
UserSuggestionDto.SUGGEST_COLUMN_IMAGE,
UserSuggestionDto.SUGGEST_COLUMN_FULLNAME)
}
}
Finally, I prefer to populate a recyclerView in a dedicated fragment for research of user.
Is There a specific way to do it ?
My fears are :
RecyclerView is not designed to use Cursor and AsyncTask (Contrary to the searchView suggestions adapter)
So how to populate a recycler view from searchView on each text query change (by calling api each time) properly ?
Thanks in advance
I need to show a custom info window when I click on a pin.
The pin is directly in style on the layer.
When I creating a map Im getting the style by url:
mapView?.getMapAsync { map ->
map.setStyle(Style.Builder().fromUrl("mapbox://styles/my-style")) {
onMapReady(map)
}
}
Then I define this layer:
fun onMapReady(mapboxMap: MapboxMap) {
this.mapboxMap = mapboxMap
val layer = mapboxMap.style?.getLayer("my-layer")
layer?.setProperties(visibility(Property.VISIBLE))
mapboxMap.addOnMapClickListener(this#InfoWindowSymbolLayerActivity)
}
OnMapClick method:
override fun onMapClick(point: LatLng): Boolean {
return mapboxMap?.projection?.toScreenLocation(point)?.let { handleClickIcon(it) }!!
}
HandleClickIcon method:
fun handleClickIcon(screenPoint: PointF): Boolean {
val features = mapboxMap?.queryRenderedFeatures(screenPoint, MARKER_LAYER_ID)
val inflater = LayoutInflater.from(this)
val bubbleLayout = inflater.inflate(R.layout.pin_info, null) as BubbleLayout
val type = features?.get(0)?.getStringProperty(type)
bubbleLayout.tvDefectType.text = type?.let { formatType(it) }
val username = features?.get(0)?.getStringProperty(username)
bubbleLayout.tvDefectInfo.text = username?.let { formatDefectInfo(it) }
val measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
bubbleLayout.measure(measureSpec, measureSpec)
val measuredWidth = bubbleLayout.measuredWidth.toFloat()
bubbleLayout.arrowPosition = measuredWidth / 2 - 5
val bitmap = SymbolGenerator.generate(bubbleLayout)
type?.let { mapboxMap?.style?.addImage(it, bitmap) }
mapboxMap?.let {
it.getStyle { style ->
setUpInfoWindowLayer(style)
}
}
return true
}
Mapbox example uses custom GeoJson:
https://docs.mapbox.com/android/maps/examples/symbol-layer-info-window/
But I need to display the info window above the
pin like this on click
If the layer is already in your style, then you don't need to set its visibility to visible with layer?.setProperties(visibility(Property.VISIBLE)). It's already going to be visible.
I'd follow https://docs.mapbox.com/android/maps/examples/symbol-layer-info-window/ by:
Starting the data loading and bubble window SymbolLayer setup earlier in your code, rather than in onMapClick: https://github.com/mapbox/mapbox-android-demo/blob/master/MapboxAndroidDemo/src/main/java/com/mapbox/mapboxandroiddemo/examples/dds/InfoWindowSymbolLayerActivity.java#L88
Only changing the select state in onMapClick: https://github.com/mapbox/mapbox-android-demo/blob/master/MapboxAndroidDemo/src/main/java/com/mapbox/mapboxandroiddemo/examples/dds/InfoWindowSymbolLayerActivity.java#L186-L206
I am trying to implement MVI Architecture in Android, but don't want to use Mosby Library. I want to learn the basics first.
I am building a sample app where when I press a button, text in the textview changes(initially the text is something else). Here is the code for MainActivity and MainPresenter.
class MainActivity : AppCompatActivity(), MainContract.View {
lateinit var mPresenter: MainContract.Presenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mPresenter = MainPresenter()
mPresenter.attachPresenter(this)
bind()
}
#SuppressLint("CheckResult")
private fun bind() {
mPresenter.states().subscribe({ state ->
render(state)
}, {
Log.e("error", "Error is: ", it)
it.printStackTrace()
})
mPresenter.addIntents(intents())
}
override fun intents(): Observable<MainIntent> {
return Observable.merge(
initialIntent(),
clickIntent()
)
}
override fun render(state: MainViewState) {
btn_show.isEnabled = state.isEnabledButton
helloWorldTextView.text = state.message
loadingIndicator.visibility = if (state.isLoading) View.VISIBLE else View.GONE
}
private fun initialIntent(): Observable<MainIntent.InitialIntent> = Observable.just(MainIntent.InitialIntent)
private fun clickIntent(): Observable<MainIntent.ClickIntent> {
return btn_show.clicks().map { MainIntent.ClickIntent("Eureka") }
}
}
class MainPresenter : MainContract.Presenter {
private val intentsSubject: PublishSubject<MainIntent> = PublishSubject.create()
override fun states(): Observable<MainViewState> {
return statesObservable
}
private lateinit var view: MainContract.View
override fun attachPresenter(view: MainContract.View) {
this.view = view
}
#SuppressLint("CheckResult")
override fun addIntents(intents: Observable<MainIntent>) {
intents.subscribe(intentsSubject)
}
private val reducer =
BiFunction { previousState: MainViewState, result: MainResult ->
when (result) {
is MainResult.InitialResult.InFlight -> previousState.copy(
isLoading = true,
message = "Initial Result",
isEnabledButton = false
)
is MainResult.InitialResult.Success -> previousState.copy(
isLoading = true,
message = "Initial Success",
isEnabledButton = true
)
is MainResult.InitialResult.Error -> previousState.copy(
isLoading = false,
message = "Error Initially",
isEnabledButton = true
)
is MainResult.ClickedResult.Success -> previousState.copy(
isLoading = false,
message = System.currentTimeMillis().toString(),
isEnabledButton = true
)
is MainResult.ClickedResult.Error -> previousState.copy(
isLoading = false,
message = "Error Clicked",
isEnabledButton = true
)
is MainResult.ClickedResult.InFlight -> previousState.copy(
isLoading = true,
message = "Clicked In Flight",
isEnabledButton = false
)
}
}
private fun actionFromIntent(intent: MainIntent): MainAction {
if (intent is MainIntent.InitialIntent) {
return MainAction.InitialAction
} else if (intent is MainIntent.ClickIntent) {
return MainAction.ClickedAction("Hello")
} else {
return MainAction.InitialAction
}
}
private var actionProcessor: ObservableTransformer<MainAction, MainResult> = ObservableTransformer { actions ->
actions.publish { shared ->
Observable.merge<MainResult>(
shared.ofType(MainAction.InitialAction::class.java).compose(initialActionProcessor),
shared.ofType(MainAction.ClickedAction::class.java).compose(clickActionProcessor)
)
}
}
private val initialActionProcessor =
ObservableTransformer<MainAction.InitialAction, MainResult.InitialResult> { action: Observable<MainAction.InitialAction> ->
action.switchMap {
Observable.just("hello initially")
.map { MainResult.InitialResult.Success(it) }
.cast(MainResult.InitialResult::class.java)
.onErrorReturn { MainResult.InitialResult.Error(it.message!!) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.startWith { MainResult.InitialResult.InFlight }
}
}
private val clickActionProcessor =
ObservableTransformer<MainAction.ClickedAction, MainResult.ClickedResult> { action: Observable<MainAction.ClickedAction> ->
Observable.just("Click").map { message ->
MainResult.ClickedResult.Success(message)
}.cast(MainResult.ClickedResult::class.java)
.onErrorReturn { MainResult.ClickedResult.Error("Error") }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.startWith { MainResult.ClickedResult.InFlight }
}
private val statesObservable: Observable<MainViewState> = compose()
private fun compose(): Observable<MainViewState> {
return intentsSubject
.map {
actionFromIntent(it)
}
.compose(actionProcessor)
.scan(MainViewState.idle(), reducer)
.distinctUntilChanged()
.replay(1)
.autoConnect(0)
}
}
Problem is that only the Inital event is fired and nothing else. The code doesn't respond to clicks, render is called only initially once.
Also, if I remove the startWith{} from the actionProcessors code responds to clicks, but only once. After that, nothing happens.
Does anyone see issue with the code? I have been trying to get my head around this problem for a while now.
My previous reply:
It's not straight answer to your question. But if you implement what's below, you probably won't have the problem you actually asked about and you'll have easier MVI solution.
You probably try to merge https://speakerdeck.com/jakewharton/the-state-of-managing-state-with-rxjava-devoxx-us-2017, http://hannesdorfmann.com/android/mosby3-mvi-1 and https://medium.com/#oldergod/managing-state-with-rxjava-b0798a6c5757 ideas.
Take a look here: https://proandroiddev.com/taming-state-in-android-with-elm-architecture-and-kotlin-part-1-566caae0f706 - it's simpler. Part 1 and 2 should be enough.
I tried the 1st approach and was repulsed by initial complexity. In 2nd approach you don't have Action, Intent, Result, but Msg instead. It's simpler to reason about.
There's also new MVI course - but haven't checked it yet.
Current approach:
I tried mentioned Elm Architecture, but it is not complete. There are at least 2 problems:
Only one request can get through queue at one moment. Some RxJava
should do the trick (groupBy with 2 streams: ui, background
probably).
parallel requests would update the same state, so you should differentiate DataStates inside your UiState. So different state for different part of UI.
Before writing actual fixes we realised, this is not the way to go ATM: announced Composables could do the MVI trick: smooth and precise data transition to specific parts of UI.
Disclaimer: moderator removed my answer which WAS actual answer. Even more, my answer moved to comment is cut down, which makes it look unfinished. That's why this post emerged once again. After you read it dear moderator, you can remove disclaimer, thanks :)
I am trying to get unique numbers from call logs of a user and show them in a RecyclerView. By unique I mean if I have shown the number, or corresponding contact once in the list, I have to skip the item. And I want to use a Cursor for showing this to make the rendering quick.
To do this, I have implemented a RecyclerView Adapter which looks like this (Only showing the relevant code, kotlin)
class CallAdapter(val activity: Activity, var cursor: Cursor) : RecyclerView.Adapter<CallAdapter.ViewHolder>() {
val handler = Handler()
val contacts = ArrayList<Contact>()
init {
cursor.moveToFirst()
UniqueLogs(this).start()
}
companion object {
const val TAG = "CallAdapter"
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(activity)
val binding = CallLogItemBinding.inflate(inflater, parent, false)
return ViewHolder(binding)
}
override fun getItemCount(): Int {
return contacts.count()
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val contact = contacts[position]
...
cursor.moveToNext()
}
class ViewHolder(val binding: CallLogItemBinding) : RecyclerView.ViewHolder(binding.root)
class UniqueLogs(val adapter: CallLogAdapterShobhit) : Thread() {
private var available = true
private val cursor = adapter.cursor
private val hash: HashMap<String, Int> = HashMap<String, Int>()
private val contacts = adapter.contacts
private var count = 0
override fun run() {
cursor.moveToFirst()
while (available && cursor.moveToNext()) {
var contact = adapter.getCurrentItemContact()
while (available && hash[contact.phoneNumber] == 1) {
if (cursor.moveToNext()) {
contact = adapter.getCurrentItemContact()
} else {
available = false
}
}
if (available) {
contacts.add(contact)
val position = count
count += 1
adapter.handler.post({
Log.d(TAG, "Position: $position Size: ${adapter.itemCount}")
adapter.notifyItemInserted(position)
})
hash[contact.phoneNumber] = 1
}
}
}
}
}
When I run this code, I am getting an exception which looks like this :-
java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder{c75176c position=5 id=-1, oldPos=4, pLpos:4 scrap [attachedScrap] tmpDetached not recyclable(1) no parent}
The position, id, oldPos, pLpos, etc. are different every time but the error is essentially the same.
I've tried writing logs as well to make sure that I am not trying to insert an element before the element is added to contacts array list, but the logs say that all's well.
Any idea what might be wrong here?
You are updating the data in a background thread, but notifying on the main thread. By the time the notification happens, you may have further altered the data. I would suggest that you do all your updates on the background and then just do a generic notifyDataSetChanged when everything is done.
Another option would be to calculate the entire new list in a background thread and then update the real data once and notify in the main thread.
Modifying the data and calling notifyItemInserted should synchronously or you will have inconsistent data.
Putting contacts.add(contact) just before notifyItemInserted should solve it.
adapter.handler.post({
Log.d(TAG, "Position: $position Size: ${adapter.itemCount}")
contacts.add(contact)
adapter.notifyItemInserted(position)
})