for purposes of Room db. I want to run some coroutines inside Recycler View.
Necessary suspend functions are handled as a class parameters:
class RecyclerAdapter (private val exist : suspend (lastName : String) -> Boolean)
And then, when needed I'm using following construction:
GlobalScope.launch(Dispatchers.IO) {
if (exist(dataSet[position].lastName))
[...]
I'm not sure if using the Global Scope is the best practice. I considered using lifecycleScope but in Adapter lifecycleOwner is not available, handling it as a parameter is not a good practice.
What would you guys suggest?
I think it goes against the single responsibility pattern, as the purpose of an adapter is mainly to take care of how the data is laid out.
I would move this information to the list of items, and do the call from the viewmodel, with:
viewModelScope.launch{}
than update a LiveData/StateFlow, observe it from the view, and submit the list to the adapter accordingly
I suggest to use:
CoroutineScope(Dispatchers.IO).launch {}
You can get LifecycleCoroutineScope in onBindViewHolder.
override fun onBindViewHolder(holder: EventViewHolder, position: Int) {
val coroutineScope =
holder.itemView.findViewTreeLifecycleOwner()?.lifecycleScope ?: CoroutineScope(Dispatchers.IO)
}
But as Róbert Nagy said, I don't think it's a good idea to deal directly with business logic inside RecyclerView.
Related
I have read the article,
It seems that State<T> is designed for #Composable.
Is it better to use State<T> in other classes such as ViewModel?
Yes, being part of the androidx.compose.runtime package State<T> was indeed intended as a value holder for composables.
If you want to publish/emit and consume "states" within ViewModels or Composables you might want to take a look at StateFlow and SharedFlow
You can either collect those as you would with any kotlin Flow<T> and use collectAsState within compose functions.
#Composable
fun YourComposable() {
val myState by viewModel.stats.collectAsState()
}
States trigger recomposable, for each screen I've always used custom data class (if it's necessary) and wrap it inside mutableStateOf(YourDataClass()) and place it in ViewModel just like we always use LiveData. And in your screen (composable) you can just val yourState = viewModel.yourState.value.
For a complete example
// ViewModel
private val _yourState: MutableState<AnimeTopState> = mutableStateOf(YourState())
val yourState: State<YourState> = _yourState
// ViewModel
// Composable
val yourState = viewModel.yourState.value
// Composable
So, state is like the way to trigger view changes on #Composable function, we cant just trigger view change with LiveData or normal value like the way we used to with XML view.
This is a question to aid understanding. I have a recyclerview and after a lot of effort I was finally able to handle clicking an item in the recyclerview. It was difficult because I do not see the logic behind how it is done. Here are some code snippits:
In MainActivity, inititialising the adapter:
// Adapter takes a lambda function as a parameter - this will go through to what happens onClick
val adapter = MetarItemAdapter{
a: String -> u.l(this, a)
}
In adapter class, the class initialisation with the required lambda function, now named onClick:
class MetarItemAdapter(
private val onClick: (String) -> Unit
) : RecyclerView.Adapter<MetarItemAdapter.MetarViewHolder>() {
...rest of adapter code
Within onBindViewHolder, here is how I have used onClick:
override fun onBindViewHolder(holder: MetarViewHolder, position: Int) {
val currentItem = metarList[position]
holder.tvMetar.text = currentItem.metar
holder.tvMetar.setOnClickListener {
onClick(holder.tvMetar.text.toString())
}
}
After looking at lots of stack overflow questions, I gather that this is the way to handle a click in a recyclerview.
My question is why we have to define a function all the way back in main activity, for it to be required to initialise the class, for it to be available throughout the class, for it just to be used for simple click handling in onBindViewHolder? Why can the code to handle a click not just be contained within onBindViewHolder?
Thank you for reading, I hope it makes sense. Happy to clarify any points.
You can put the Function where you want, but it should be accessible from "Adapter.onBindViewHolder()" method.
If you place your "onClick()" lambda INSIDE the Adapter Class then it will be difficult to interact (if needed) with something OUTSIDE this Adapter. Infact it's common pratice to mark extended Adapter Class (your "MetarItemAdapter") as STATIC due to better performance and less memory consumption. If the Class is Static you will not "touch" nothing outside the Adapter because UserInterface is usually instantiated in one Object (an Activity, a Fragment, etc...), but if you pass the Function as an argument it could be created from everywhere.
so in MVVM architecture even in google samples we can see things like this:
class CharacterListActivity :BaseActivity() {
val ViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.getData() // Bad!!!
...
viewModel.state.observe(this) { state ->
when(state) { // handling state is not views job
Success -> { navigatetoNextPage() } // navigating is not views job
Progress -> { showProgress() }
NetworkError -> { ShowSnackbar(viewModel.error) } // I,m not sure about this one either
Error -> { showErrorDialog(viewModel.error)
}
}
We know that any architecture has its own rules that makes the code testable, maintainable, and scalable over time.
in MVVM pattern according to both Wikipedia and Microsoft docs this is the View:
the view is the structure, layout, and appearance of what a user sees on the screen.[6] It displays a representation of the model and receives the user's interaction with the view (clicks, keyboard, gestures, etc.), and it forwards the handling of these to the view model via the data binding (properties, event callbacks, etc.) that is defined to link the view and view model.
each view is defined in XAML, with a limited code-behind that does not contain business logic. However, in some cases, the code-behind might contain UI logic that implements visual behavior such as animations.
XAML is a Xamarin thing, so now let's get back to our code:
here, since activity decides what to do with the state, the activity works as Controller like in MVC but, activity supposed to be the View ,view is just supposed to do the UI logic.
the activity even tells the ViewModel to get data. this is again not the View's job.
please note that telling what to do to the other modules in the code is not the View's job. this is making the view act as controller. view is supposed to handle its state via callbacks from the ViewModel.
the View is supposed to just tell the ViewModel about events like onClick().
since ViewModel doesn't have access to View, it can't show a dialog or navigate through the app directly!
so what is an alternative approach to do this without violation of architecture rules? should I have a function for any lif cycle event in ViewModel, like viewModel.onCreate? or viewModel.onStart? what about navigation or showing dialogs?
For The Record I'm not mixing Up mvc and mvvm, I'm saying that this pattern does which is recommended buy google.
This is not opinion-based, surely anyone can have their own implementation of any architecture but the rules must always be followed to achieve overtime maintainability.
I can name the violations in this code one by one for you:
1) UI is not responsible for getting data, UI just needs to tell ViewModel about events.
2) UI is not responsible for handling state which is exactly what it does here. more general, UI shouldn't contain any non-UI logic.
3) UI is not responsible for navigating between screens
the activity even tells the ViewModel to get data. this is again not the View's job.
Correct. The data fetch should be triggered either by ViewModel.init, or more accurately the activation of a reactive data source (modeled by LiveData, wrapping said reactive source with onActive/onInactive).
If the fetch MUST happen as a result of create, which is unlikely, then it could be done using the DefaultLifecycleObserver using the Jetpack Lifecycle API to create a custom lifecycle-aware component.
Refer to https://stackoverflow.com/a/59109512/2413303
since ViewModel doesn't have access to View, it can't show a dialog or navigate through the app directly!
You can use a custom lifecycle aware component such as EventEmitter (or here) to send one-off events from the ViewModel to the View.
You can also refer to a slightly more advanced technique where rather than just an event, an actual command is sent down in the form of a lambda expression sent as an event, which will be handled by the Activity when it becomes available.
Refer to https://medium.com/#Zhuinden/simplifying-jetpack-navigation-between-top-level-destinations-using-dagger-hilt-3d918721d91e
typealias NavigationCommand = NavController.() -> Unit
#ActivityRetainedScoped
class NavigationDispatcher #Inject constructor() {
private val navigationEmitter: EventEmitter<NavigationCommand> = EventEmitter()
val navigationCommands: EventSource<NavigationCommand> = navigationEmitter
fun emit(navigationCommand: NavigationCommand) {
navigationEmitter.emit(navigationCommand)
}
}
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
#Inject
lateinit var navigationDispatcher: NavigationDispatcher
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
navigationDispatcher.navigationCommands.observe(this) { command ->
command.invoke(Navigation.findNavController(this, R.id.nav_host))
}
}
}
class LoginViewModel #ViewModelInject constructor(
private val navigationDispatcher: NavigationDispatcher,
#Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
fun onRegisterClicked() {
navigationDispatcher.emit {
navigate(R.id.logged_out_to_registration)
}
}
}
If Hilt is not used, the equivalent can be done using Activity-scoped ViewModel and custom AbstractSavedStateViewModelFactory subclasses.
So my general question is how to call a function from view model for #Query where you have to pass something and then return something.
My simple example:
DAO
#Query ("SELECT * FROM table_name WHERE id = :id LIMIT 1")
fun getItemById (id: Long) : MyItem
Repo
fun getItemById (id: Long) : MyItem {
return itemDao.getItemById(id)
}
I know that it cannot and should not be done on ui thread. For inserting and deleting an item i use viewModelScope job but i cannot (maybe just don`t know how to) use it to return anything.
If i return it everywhere as LiveData, then it works just like that:
ViewModel
fun itemById(id: Long): LiveData<MyItem> {
return itemRepo.getItemById(id)
}
And then i observe it in a Fragment/Activity:
viewModel.itemById(id).observe(this, Observer {
// using it
})
The thing is, that i dont really need it to be an observable livedata. I only need to get it once, check condition and thats it.
So maybe someone could recommend how to do it, without it being a livedata. Or should i leave it a live data?
If you want to get the update only once, then I recommend SingleLiveEvent instead of LiveData.
Here is the class provided by google: Github link
A blog on how to use it: Link
The only drawback of SingleLiveEvent is that it can't have multiple observers.
If you don't like LiveData, you could try RxJava's Single [Observable]
Recently, I´ve read about how important it is to have a Single-Source-Of-Truth (SSOT) when designing an app´s backend (repository, not server-side-backend). https://developer.android.com/topic/libraries/architecture/guide.html
By developing a news-feed app (using the awesome https://newsapi.org/) I am trying to learn more about app architecture.
However, I am unsure of how to design the repository interface for my app.
Btw.: I am using MVVM for my presentation layer. The View subscribes to the ViewModel´s LiveData. The ViewModel subscribes to RxJava streams.
So I came up with 2 approaches:
Approach 1:
interface NewsFeedRepository {
fun loadFeed(): Flowable<List<Article>>
fun refreshFeed(): Completable
fun loadMore(): Completable
}
interface SearchArticleRepository {
fun searchArticles(sources: List<NewsSource>? = null, query: String? = null): Flowable<List<Article>>
fun moreArticles(): Completable
}
interface BookmarkRepository {
fun getBookmarkedArticles(): Flowable<List<Article>>
fun bookmarkArticle(id: String): Completable
}
This approach is primarily using Flowables which emit data if the corresponding data in the underlying SSOT (database) changes (e.g old data gets replaced with fresh data from API, more data was loaded from API, ...). However, I am unsure if using a Flowable for SearchArticleRepository#searchArticles(...) makes sense. As it is like some request/response thing, where maybe a Single might me be more intuitive.
Approach 2:
interface NewsFeedRepository {
fun loadFeed(): Single<List<Article>>
fun refreshFeed(): Single<List<Article>>
fun loadMore(): Single<List<Article>>
}
interface SearchArticleRepository {
fun searchArticles(sources: List<NewsSource>? = null, query: String? = null): Single<List<Article>>
fun moreArticles(): Single<List<Article>>
}
interface BookmarkRepository {
fun getBookmarkedArticles(): Single<List<Article>>
fun bookmarkArticle(id: String): Single<Article> // Returns the article that was modified. Articles are immutable.
}
This approach is using Singles instead of Flowables. This seems very intuitive but if the data in the SSOT changes, no changes will be emitted. Instead, a call to the repository has to be made again. Another aspect to take into account is that the ViewModel may have to manage its own state.
Let´s take the FeedViewModel for example (pseudo-code).
class FeedViewModel : ViewModel() {
// Variables, Boilerplate, ...
val newsFeed: LiveData<List<Article>>
private val articles = mutableListOf<Article>()
fun loadNewsFeed() {
// ...
repository.loadFeed()
//...
// On success, clear the feed and append the loaded articles.
.subscribe({articles.clear(); articles.addAll(it)})
// ...
}
fun loadMore() {
// ...
repository.loadMore()
//...
// On success, append the newly loaded articles to the feed.
.subscribe({articles.addAll(it)})
// ...
}
}
So this might not be crucial for a smaller app like mine, but it definitely can get a problem for a larger app (see state management: http://hannesdorfmann.com/android/arch-components-purist).
Finally, I wanted to know which approach to take and why. Are there any best-practices? I know many of you have already done some larger software-projects/apps and it would be really awesome if some of you could share some knowledge with me and others.
Thanks a lot!
I'd rather go for the first approach using Observables instead of Flowables in your case:
interface NewsFeedRepository {
fun loadFeed(): Observable<List<Article>>
fun refreshFeed(): Completable
fun loadMore(): Completable
}
interface SearchArticleRepository {
fun searchArticles(sources: List<NewsSource>? = null, query: String? = null): Observable<List<Article>>
fun moreArticles(): Completable
}
interface BookmarkRepository {
fun getBookmarkedArticles(): Observable<List<Article>>
fun bookmarkArticle(id: String): Completable
}
I don't see any reason you should necessarily use Flowable for this purpose since you'll never have any OOME related issues checking your repository changes. In other words, for your use case IMHO backpressure is not necessary at all.
Check this official guide which gives us an advice of when to a Flowable over an Observable.
On the other hand, and not related to the question itself, I have serious doubts of what's the purpose of loadMore or moreArticles methods since they return a Completable. Without knowing the context, it may seem you could either refactor the method name by a better name or change the return type if they do what they seem to do by the name.
I believe the first approach is better, Your repo will update the data whenever the data is changed and your view model will be notified automatically and that's cool, while in your second approach you have to call the repo again and that's not really reactive programming.
Also, assume that the data can be changed by something rather than load more event from the view, like when new data added to the server, or some other part of the app changes the data, Now in the first approach again you get the data automatically while for the second your not even know about the changed data and you don't know when to call the method again.