I got an android project I'm beginning to work on, and I want its structure to be as robust as possible.
I'm coming from a WPF MVVM background and I've been reading a little about android applications architecture, but I just couldn't find a straight clear answer about which architecture I should use.
Some people suggested using MVVM - http://vladnevzorov.com/2011/04/30/android-application-architecture-part-ii-architectural-styles-and-patterns/
and others suggested using MVC, but didn't specify how exactly it should be implemented.
As I said I'm coming from a WPF-MVVM background, and therefore I know it heavily relies on bindings which as far as I understand, are not supported by default in Android.
It seems like there is a 3rd party solution - http://code.google.com/p/android-binding/
But I don't know if I'd like to rely on that. What if its development would stop and it will not be supported by future APIs and etc..
Basically what I'm looking for is a thorough tutorial that will teach me the best practices for building the application's structure. Folders and classes structure and etc. I just couldn't find any thorough tutorial, and I would have expected that Google would supply such a tutorial for its developers. I just don't think that this kind of documentation handles the technical aspect good enough - http://developer.android.com/guide/topics/fundamentals.html
I hope I've been clear enough and that I'm not asking for too much, I just want to be sure about my application's structure, before my code will turn into a spaghetti monster.
Thanks!
First of all, Android doesn't force you to use any architecture. Not only that but it also makes it somewhat difficult to try to follow to any. This will require you to be a smart developer in order to avoid creating a spaghetti codebase :)
You can try to fit in any pattern you know and you like. I find that the best approach will in some way get into your guts as you develop more and more applications (sorry about that but as always, you'll have to make lots of mistakes until you start doing it right).
About the patterns you know, let me do something wrong: I'll mix three different patterns so you get the feeling of what does what in android. I believe the Presenter/ModelView should be somewhere in the Fragment or Activity. Adapters might sometimes do this job as they take care of inputs in lists. Probably Activities should work like Controllers too. Models should be regular java files whereas the View should lay in layout resources and some custom components you might have to implement.
I can give you some tips. This is a community wiki answer so hopefully other people might include other suggestions.
File Organization
I think there are mainly two sensible possibilities:
organize everything by type - create a folder for all activities, another folder for all adapters, another folder for all fragments, etc
organize everything by domain (maybe not the best word). This would mean everything related to "ViewPost" would be inside the same folder - the activity, the fragment, the adapters, etc. Everything related to "ViewPost" would be in another folder. Same for "EditPost", etc. I guess activities would mandate the folders you'd create and then there would be a few more generic ones for base classes for example.
Personally, I have only been involved in projects using the first approach but I really would like to try the later as I believe it could make things more organized. I see no advantage in having a folder with 30 unrelated files but that's what I get with the first approach.
Naming
When creating layouts and styles, always name (or identify them) using a prefix for the activity (/fragment) where they are used.
So, all strings, styles, ids used in the context of "ViewPost" should start be "#id/view_post_heading" (for a textview for example), "#style/view_post_heading_style", "#string/view_post_greeting".
This will optimize autocomplete, organization, avoid name colision, etc.
Base Classes
I think you'll want to use base classes for pretty much everything you do: Adapters, Activities, Fragments, Services, etc. These might be useful at least for debugging purposes so you know which events are happening in all your activity.
General
I never use anonymous classes - these are ugly and will drive your attention away when you are trying to read the code
Sometimes I prefer to use inner classes (compared to create a dedicated class) - if a class is not going to be used anywhere else (and it's small) I think this is very handy.
Think about your logging system from the beginning - you can use android's logging system but make a good use of it!
I think it would be more helpful to explain MVVM in android through an example. The complete article together with the GitHub repo info is here for more info.
Let’s suppose the same benchmark movie app example introduced in the first part of this series. User enters a search term for a movie and presses the ‘FIND’ button, based on which the app searches for the list of movies including that search term and shows them. Clicking on each movie on the list then shows its details.
I will now explain how this app is implemented in MVVM followed by the complete android app, which is available on my GitHub page.
When the user clicks on the ‘FIND’ button on the View, a method is called from the ViewModel with the search term as its argument:
main_activity_button.setOnClickListener({
showProgressBar()
mMainViewModel.findAddress(main_activity_editText.text.toString())
})
The ViewModel then calls the findAddress method from the Model to search for the movie name:
fun findAddress(address: String) {
val disposable: Disposable = mainModel.fetchAddress(address)!!.subscribeOn(schedulersWrapper.io()).observeOn(schedulersWrapper.main()).subscribeWith(object : DisposableSingleObserver<List<MainModel.ResultEntity>?>() {
override fun onSuccess(t: List<MainModel.ResultEntity>) {
entityList = t
resultListObservable.onNext(fetchItemTextFrom(t))
}
override fun onError(e: Throwable) {
resultListErrorObservable.onNext(e as HttpException)
}
})
compositeDisposable.add(disposable)
}
When the response comes from the Model, the onSuccess method of the RxJava observer carries the successful result, but as the ViewModel is View agnostic, it does not have or use any View instance to pass the result for showing. It instead triggers an event in the resultListObservable by calling resultListObservable.onNext(fetchItemTextFrom(t)) , which is observed by the View:
mMainViewModel.resultListObservable.subscribe({
hideProgressBar()
updateMovieList(it)
})
So the observable plays a mediator role between the View and ViewModel:
ViewModel triggers an event in its observable
View updates the UI by subscribing to ViewModel’s observable
Here’s the full code for the View. In this example, View is an Activity class, but Fragment can also be equally used:
class MainActivity : AppCompatActivity() {
private lateinit var mMainViewModel: MainViewModel
private lateinit var addressAdapter: AddressAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mMainViewModel = MainViewModel(MainModel())
loadView()
respondToClicks()
listenToObservables()
}
private fun listenToObservables() {
mMainViewModel.itemObservable.subscribe(Consumer { goToDetailActivity(it) })
mMainViewModel.resultListObservable.subscribe(Consumer {
hideProgressBar()
updateMovieList(it)
})
mMainViewModel.resultListErrorObservable.subscribe(Consumer {
hideProgressBar()
showErrorMessage(it.message())
})
}
private fun loadView() {
setContentView(R.layout.activity_main)
addressAdapter = AddressAdapter()
main_activity_recyclerView.adapter = addressAdapter
}
private fun respondToClicks() {
main_activity_button.setOnClickListener({
showProgressBar()
mMainViewModel.findAddress(main_activity_editText.text.toString())
})
addressAdapter setItemClickMethod {
mMainViewModel.doOnItemClick(it)
}
}
fun showProgressBar() {
main_activity_progress_bar.visibility = View.VISIBLE
}
fun hideProgressBar() {
main_activity_progress_bar.visibility = View.GONE
}
fun showErrorMessage(errorMsg: String) {
Toast.makeText(this, "Error retrieving data: $errorMsg", Toast.LENGTH_SHORT).show()
}
override fun onStop() {
super.onStop()
mMainViewModel.cancelNetworkConnections()
}
fun updateMovieList(t: List<String>) {
addressAdapter.updateList(t)
addressAdapter.notifyDataSetChanged()
}
fun goToDetailActivity(item: MainModel.ResultEntity) {
var bundle = Bundle()
bundle.putString(DetailActivity.Constants.RATING, item.rating)
bundle.putString(DetailActivity.Constants.TITLE, item.title)
bundle.putString(DetailActivity.Constants.YEAR, item.year)
bundle.putString(DetailActivity.Constants.DATE, item.date)
var intent = Intent(this, DetailActivity::class.java)
intent.putExtras(bundle)
startActivity(intent)
}
class AddressAdapter : RecyclerView.Adapter<AddressAdapter.Holder>() {
var mList: List<String> = arrayListOf()
private lateinit var mOnClick: (position: Int) -> Unit
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): Holder {
val view = LayoutInflater.from(parent!!.context).inflate(R.layout.item, parent, false)
return Holder(view)
}
override fun onBindViewHolder(holder: Holder, position: Int) {
holder.itemView.item_textView.text = mList[position]
holder.itemView.setOnClickListener { mOnClick(position) }
}
override fun getItemCount(): Int {
return mList.size
}
infix fun setItemClickMethod(onClick: (position: Int) -> Unit) {
this.mOnClick = onClick
}
fun updateList(list: List<String>) {
mList = list
}
class Holder(itemView: View?) : RecyclerView.ViewHolder(itemView)
}
}
Here is the ViewModel:
class MainViewModel() {
lateinit var resultListObservable: PublishSubject<List<String>>
lateinit var resultListErrorObservable: PublishSubject<HttpException>
lateinit var itemObservable: PublishSubject<MainModel.ResultEntity>
private lateinit var entityList: List<MainModel.ResultEntity>
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
private lateinit var mainModel: MainModel
private val schedulersWrapper = SchedulersWrapper()
constructor(mMainModel: MainModel) : this() {
mainModel = mMainModel
resultListObservable = PublishSubject.create()
resultListErrorObservable = PublishSubject.create()
itemObservable = PublishSubject.create()
}
fun findAddress(address: String) {
val disposable: Disposable = mainModel.fetchAddress(address)!!.subscribeOn(schedulersWrapper.io()).observeOn(schedulersWrapper.main()).subscribeWith(object : DisposableSingleObserver<List<MainModel.ResultEntity>?>() {
override fun onSuccess(t: List<MainModel.ResultEntity>) {
entityList = t
resultListObservable.onNext(fetchItemTextFrom(t))
}
override fun onError(e: Throwable) {
resultListErrorObservable.onNext(e as HttpException)
}
})
compositeDisposable.add(disposable)
}
fun cancelNetworkConnections() {
compositeDisposable.clear()
}
private fun fetchItemTextFrom(it: List<MainModel.ResultEntity>): ArrayList<String> {
val li = arrayListOf<String>()
for (resultEntity in it) {
li.add("${resultEntity.year}: ${resultEntity.title}")
}
return li
}
fun doOnItemClick(position: Int) {
itemObservable.onNext(entityList[position])
}
}
and finally the Model:
class MainModel {
private var mRetrofit: Retrofit? = null
fun fetchAddress(address: String): Single<List<MainModel.ResultEntity>>? {
return getRetrofit()?.create(MainModel.AddressService::class.java)?.fetchLocationFromServer(address)
}
private fun getRetrofit(): Retrofit? {
if (mRetrofit == null) {
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
val client = OkHttpClient.Builder().addInterceptor(loggingInterceptor).build()
mRetrofit = Retrofit.Builder().baseUrl("http://bechdeltest.com/api/v1/").addConverterFactory(GsonConverterFactory.create()).addCallAdapterFactory(RxJava2CallAdapterFactory.create()).client(client).build()
}
return mRetrofit
}
class ResultEntity(val title: String, val rating: String, val date: String, val year: String)
interface AddressService {
#GET("getMoviesByTitle")
fun fetchLocationFromServer(#Query("title") title: String): Single<List<ResultEntity>>
}
}
Full article here
Related
I am trying to implement an infinite list with the Paging library, MVVM and LiveData.
In my View (in my case my fragment) I ask for data from the ViewModel and observe the changes:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.getItems("someSearchQuery")
viewModel.pagedItems.observe(this, Observer<PagedList<Item>> {
// ItemPagedRecyclerAdapter
// EDIT --> Found this in the official Google example
// Workaround for an issue where RecyclerView incorrectly uses the loading / spinner
// item added to the end of the list as an anchor during initial load.
val layoutManager = (recycler.layoutManager as LinearLayoutManager)
val position = layoutManager.findFirstCompletelyVisibleItemPosition()
if (position != RecyclerView.NO_POSITION) {
recycler.scrollToPosition(position)
}
})
}
In the ViewModel I fetch my data like this:
private val queryLiveData = MutableLiveData<String>()
private val itemResult: LiveData<LiveData<PagedList<Item>>> = Transformations.map(queryLiveData) { query ->
itemRepository.fetchItems(query)
}
val pagedItems: LiveData<PagedList<Item>> = Transformations.switchMap(itemResult) { it }
private fun getItems(queryString: String) {
queryLiveData.postValue(queryString)
}
In the repository I fetch for the data with:
fun fetchItems(query: String): LiveData<PagedList<Item>> {
val boundaryCallback = ItemBoundaryCallback(query, this.accessToken!!, remoteDataSource, localDataSource)
val dataSourceFactory = localDataSource.fetch(query)
return dataSourceFactory.toLiveData(
pageSize = Constants.PAGE_SIZE_ITEM_FETCH,
boundaryCallback = boundaryCallback)
}
As you might have already noticed, I used the Codelabs from Google as an example, but sadly I could not manage to make it work correctly.
class ItemBoundaryCallback(
private val query: String,
private val accessToken: AccessToken,
private val remoteDataSource: ItemRemoteDataSource,
private val localDataSource: Item LocalDataSource
) : PagedList.BoundaryCallback<Item>() {
private val executor = Executors.newSingleThreadExecutor()
private val helper = PagingRequestHelper(executor)
// keep the last requested page. When the request is successful, increment the page number.
private var lastRequestedPage = 0
private fun requestAndSaveData(query: String, helperCallback: PagingRequestHelper.Request.Callback) {
val searchData = SomeSearchData()
remoteDataSource.fetch Items(searchData, accessToken, lastRequestedPage * Constants.PAGE_SIZE_ITEMS_FETCH, { items ->
executor.execute {
localDataSource.insert(items) {
lastRequestedPage++
helperCallback.recordSuccess()
}
}
}, { error ->
helperCallback.recordFailure(Throwable(error))
})
}
#MainThread
override fun onZeroItemsLoaded() {
helper.runIfNotRunning(PagingRequestHelper.RequestType.INITIAL) {
requestAndSaveData(query, it)
}
}
#MainThread
override fun onItemAtEndLoaded(itemAtEnd: Item) {
helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) {
requestAndSaveData(query, it)
}
}
My adapter for the list data:
class ItemPagedRecyclerAdapter : PagedListAdapter<Item, RecyclerView.ViewHolder>(ITEM_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return ItemViewHolder(parent)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
(holder as ItemViewHolder).bind(item, position)
}
}
companion object {
private val ITEM_COMPARATOR = object : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean =
olItem.id == newItem.id
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean =
oldItem == newItem
}
}
}
My problem right now is: The data is fetched and saved locally and is even displayed correctly in my list. But the data seems to be "looping", so there is always the same data showing despite there are different objects in the database (I checked with Stetho, about several hundred). Curiously the last item in the list is also always the same and sometimes there are items reloading while scrolling. Another problem is that it stops reloading at some point (sometimes 200, sometimes 300 data items).
I thought it might be because my ITEM_COMPARATOR was checking wrongly and returning the wrong boolean, so I set both to return true just to test, but this changed nothing.
I was also thinking of adding a config to the LivePagedListBuilder, but this also changed nothing. So I am a little bit stuck. I also looked into some examples doing it with a PageKeyedDataSource etc., but Google's example is working without it, so I want to know why my example is not working.
https://codelabs.developers.google.com/codelabs/android-paging/index.html?index=..%2F..index#5
Edit:
Google does have another example in their blueprints. I added it to the code. https://github.com/android/architecture-components-samples/blob/master/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ui/RedditActivity.kt.
Now it is loading correctly, but when the loading happens, some items in the list still flip.
Edit 2:
I edited the BoundaryCallback, still not working (now provided the PagingRequestHelper suggested by Google).
Edit 3:
I tried it just with the remote part and it works perfectly. There seems to be a problem with Room/the datasource that room provides.
Ok, just to complete this issue, I found the solution.
To make this work, you must have a consistent list-order from your backend/api-data. The flipping was caused by the data that was constantly sent in another order than before and therefore made some items in the list "flip" around.
So you have to save an additional field to your data (an index-like field) to order your data accordingly. The fetch from the local database in the DAO is then done with a ORDER BY statement. I hope I can maybe help someone who forgot the same as I did:
#Query("SELECT * FROM items ORDER BY indexFromBackend")
abstract fun fetchItems(): DataSource.Factory<Int, Items>
You have to override PageKeyedDataSource to implement paging logic with Paging library. Check out this link
I'm working with MVVM, and I have made different implementations of it, but one thing that is still making me doubt is how do I get data from a Repository (Firebase) from my ViewModel without attaching any lifecycle to the ViewModel.
I have implemented observeForever() from the ViewModel, but I don't think that is a good idea because I think I should communicate from my repository to my ViewModel either with a callback or a Transformation.
I leave here an example where I fetch a device from Firebase and update my UI, if we can see here, I'm observing the data coming from the repo from the UI, but from the ViewModel I'm also observing data from the repo, and here is where I really doubt if I'm using the right approach, since I don't know if observeForever() will be cleared on onCleared() if my view is destroyed, so it won't keep the observer alive if the view dies.
UI
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
val deviceId = editText.text.toString().trim()
observeData(deviceId)
}
}
fun observeData(deviceId:String){
viewModel.fetchDeviceData(deviceId).observe(this, Observer {
textView.text = "Tipo: ${it.devType}"
})
ViewModel
class MainViewmodel: ViewModel() {
private val repo = Repo()
fun fetchDeviceData(deviceId:String):LiveData<Device>{
val mutableData = MutableLiveData<Device>()
repo.getDeviceData(deviceId).observeForever {
mutableData.value = it
}
return mutableData
}
}
Repository
class Repo {
private val db = FirebaseDatabase.getInstance().reference
fun getDeviceData(deviceId:String):LiveData<Device>{
val mutableData = MutableLiveData<Device>()
db.child(deviceId).child("config/device").addListenerForSingleValueEvent(object: ValueEventListener{
override fun onDataChange(dataSnapshot: DataSnapshot) {
val device = dataSnapshot.getValue(Device::class.java)
mutableData.value = device
}
override fun onCancelled(dataError: DatabaseError) {
Log.e("Error","handle error callback")
}
})
return mutableData
}
}
This example just shows how to fetch the device from Firebase, it works, but from my ViewModel, it keeps making me think that observeForever() is not what I'm looking for to communicate data between the repository to the ViewModel.
I have seen Transformations, but I, in this case, I just need to deliver the entire Device object to my UI, so I don't need to transform the Object I'm retrieving to another Object
What should be here the right approach to communicate the repository and the ViewModel properly?
is observeForever lifecycle aware?
No, that's why it's called observeForever.
I have implemented observeForever() from the ViewModel, but I don't think that is a good idea
No, it's not, you should be using Transformations.switchMap {.
since I don't know if observeForever() will be cleared on onCleared() if my view is destroyed, so it won't keep the observer alive if the view dies.
Well if you're not clearing it in onCleared() using removeObserver(observer), then it won't clear itself, because it observes forever.
here is where I really doubt if I'm using the right approach,
No, you can do much better than this following a reactive approach.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
val deviceId = editText.text.toString().trim()
viewModel.onSelectedDeviceChanged(deviceId)
}
viewModel.selectedDevice.observe(this, Observer { device ->
textView.text = "Tipo: ${device.devType}"
})
}
And
class MainViewModel(
private val savedStateHandle: SavedStateHandle,
): ViewModel() {
private val repo = Repo() // TODO: move to Constructor Argument with ViewModelProvider.Factory
private val selectedDeviceId: MutableLiveData<String> = savedStateHandle.getLiveData<String>("selectedDeviceId")
fun onSelectedDeviceChanged(deviceId: String) {
selectedDeviceId.value = deviceId
}
val selectedDevice = Transformations.switchMap(selectedDeviceId) { deviceId ->
repo.getDeviceData(deviceId)
}
}
And
class Repo {
private val db = FirebaseDatabase.getInstance().reference // TODO: move to constructor arg? Probably
fun getDeviceData(deviceId:String) : LiveData<Device> {
return object: MutableLiveData<Device>() {
private val mutableLiveData = this
private var query: Query? = null
private val listener: ValueEventListener = object: ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val device = dataSnapshot.getValue(Device::class.java)
mutableLiveData.value = device
}
override fun onCancelled(dataError: DatabaseError) {
Log.e("Error","handle error callback")
}
}
override fun onActive() {
query?.removeEventListener(listener)
val query = db.child(deviceId).child("config/device")
this.query = query
query.addValueEventListener(listener)
}
override fun onInactive() {
query?.removeEventListener(listener)
query = null
}
}
}
}
This way, you can observe for changes made in Firebase (and therefore be notified of future changes made to your values) using LiveData, rather than only execute a single fetch and then not be aware of changes made elsewhere to the same data.
To use ObserveForever, you need to remove the observer inside onClear in the ViewModel.
In this case, I would suggest to use Transformation even though you just need a direct mapping without any processing of the data, which is actually the same as what you are doing with the observer for observerForever.
observeForever() is not Lifecycle aware and will continue to run until removeObserver() is called.
In your ViewModel do this instead,
class MainViewmodel: ViewModel() {
private val repo = Repo()
private var deviceData : LiveData<Device>? = null
fun fetchDeviceData(deviceId:String):LiveData<Device>{
deviceData = repo.getDeviceData(deviceId)
return deviceData!!
}
}
The new paging library allows us to specify a custom data source to use with data pagination. Paging library documentation and sample code on github show us how to create your custom data source instances by creating a subclass of DataSource.Factory like so:
class ConcertTimeDataSourceFactory(private val concertStartTime: Date) :
DataSource.Factory<Date, Concert>() {
val sourceLiveData = MutableLiveData<ConcertTimeDataSource>()
override fun create(): DataSource<Date, Concert> {
val source = ConcertTimeDataSource(concertStartTime)
sourceLiveData.postValue(source)
return source
}
}
In a real app, you'd generally have multiple views with recyclerviews and hence multiple custom data sources. So, do you end up creating multiple implementations of DataSource.Factory per data source or is there a more generic solution?
Not always.
If you are using other Android Architecture components or libraries that give it good support, in most cases the DataSource.Factory will be delivered as a result of a method call like Room database does.
If you really want a very generic one and have no problem with reflection:
class GenericFactory<K, R>(private val kClass: KClass<DataSource<K, R>>) : DataSource.Factory<K, R>() {
override fun create(): DataSource<K, R> = kClass.java.newInstance()
}
Your example shows a DataSource.Factory that exposes the DataSource as a LiveData. This is just necessary in specific cases, for example, when the DataSource holds a retry method for the API call. In other cases, your DataSource.Factory will be as simple as 3 more lines in your DataSource:
class MySimpleDataSource<R> : PageKeyedDataSource<String, R>() {
override fun loadBefore(params: LoadParams<String>,
callback: LoadCallback<String, R>) {
// do your thing
}
override fun loadAfter(params: LoadParams<String>,
callback: LoadCallback<String, R>) {
// do your thing
}
override fun loadInitial(params: LoadInitialParams<String>,
callback: LoadInitialCallback<String, R>) {
// do your thing
}
class Factory<R> : DataSource.Factory<String, R>() {
override fun create(): DataSource<String, R> = MySimpleDataSource<R>()
}
}
I guess the most common case for custom DataSource.Factory is paginated REST API calls. In this case, you may just implement one generic DataSource and one DataSource.Factory that receives the request object and response callback as a lambda.
data class MyCollection<R>(
var items: List<R>,
var nextPageToken: String
)
data class MyData(
var title: String = ""
)
abstract class SomeLibraryPagedClientRequest<R> {
abstract fun setNextPageToken(token: String?): SomeLibraryPagedClientRequest<R>
abstract fun enqueue(callback: (response: Response<R>) -> Unit): Unit
}
class MyRestApiDataSource(
private val request: SomeLibraryPagedClientRequest<MyData>,
private val handleResponse: (Response<R>) -> Unit
) : ItemKeyedDataSource<String, MyData>() {
var nextPageToken: String = ""
override fun getKey(item: MyData): String = nextPageToken
override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<MyData>) {
}
override fun loadInitial(params: LoadInitialParams<String>, callback: LoadInitialCallback<MyData>) {
request.setNextPageToken(params.requestedInitialKey).enqueue { data ->
nextPageToken = response.data.nextPageToken
if(response.isSucefull) callback.onResult(response.data.items)
handleResponse.invoke(response)
}
}
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<MyData>) {
request.setNextPageToken(params.key).enqueue { response ->
nextPageToken = response.data.nextPageToken
if(response.isSucefull) callback.onResult(response.data.items)
handleResponse.invoke(response)
}
}
class Factory<R>(
private val request: SomeLibraryPagedClientRequest<MyData>,
private val handleResponse: (Response<R>) -> Unit
) : DataSource.Factory<String, R>() {
override fun create(): DataSource<String, R> = MySimpleDataSource<R>()
}
}
We can create multiple instances of DataSource.Factory class which holds multilpe LiveData objects.
First create instance of factory and viewmodel in main activity then write a switch condition or if else ladder for choosing data source from DataSource.Factory class.
In switch condition you need to call factory.create(viewmodel).getLiveData method
For example
switch (service){
case 1:
final Adapter adapter = new Adapter();
factory.create(viewModel.getClass()).getPagedECListLiveData().observe((LifecycleOwner) activity, new Observer<PagedList<ECRecord>>() {
#Override
public void onChanged(#Nullable PagedList<ECRecord> ecRecords) {
Adapter.submitList(ecRecords);
}
});
recyclerView.setAdapter(adapter);
break;
case 2:
final CAdapter cadapter = new CAdapter();
factory.create(viewModel.getClass()).getPagedSTListLiveData().observe((LifecycleOwner) activity, new Observer<PagedList<STRecord>>() {
#Override
public void onChanged(#Nullable PagedList<STRecord> stRecords) {
ECTAdapter.submitList(stRecords);
}
});
recyclerView.setAdapter(cadapter);
break;
}
Happy Coding :)
As seen in the Guide to the app architecture it's advised to have a single source of truth so no matter how many data sources you have you should only have one single source of truth.
Examples used in the Paging Library all rely on this fact and that is why paging library support Room by default. But it don't means you have to use database, as a matter of fact:
In this model, the database serves as the single source of truth, and
other parts of the app access it via the repository. Regardless of
whether you use a disk cache, we recommend that your repository
designate a data source as the single source of truth to the rest of
your app.
P.S: Even if you don't want to designate a single source of truth you don't have to define multiple DataSource you can just implement a custom data source that combine multiple stream of data to create a displayable list of items. For example:
public class MentionKeyedDataSource extends ItemKeyedDataSource<Long, Mention> {
private Repository repository;
...
private List<Mention> cachedItems;
public MentionKeyedDataSource(Repository repository, ..., List<Mention> cachedItems){
super();
this.repository = repository;
...
this.cachedItems = new ArrayList<>(cachedItems);
}
#Override
public void loadInitial(#NonNull LoadInitialParams<Long> params, final #NonNull ItemKeyedDataSource.LoadInitialCallback<Mention> callback) {
Observable.just(cachedItems)
.filter(() -> return cachedItems != null && !cachedItems.isEmpty())
.switchIfEmpty(repository.getItems(params.requestedLoadSize))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(response -> callback.onResult(response.data.list));
}
...
Activity receiving intent
class AddNoteActivity : AppCompatActivity() {
private lateinit var addViewModel: NoteViewModel
private lateinit var titleEditText: TextInputEditText
private lateinit var contentEditText: TextInputEditText
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_note_activty)
setSupportActionBar(toolbar)
addViewModel = ViewModelProviders.of(this).get(NoteViewModel::class.java)
titleEditText = findViewById(R.id.itemTitle)
contentEditText = findViewById(R.id.itemNote)
val extra = intent.extras
if (extra != null) {
val uuid = extra.getLong("UUID")
val note: Notes? = addViewModel.getNote(uuid)
titleEditText.setText(note!!.title)
contentEditText.setText(note.note)
}
}
}
NoteViewModel class
class NoteViewModel(application: Application) : AndroidViewModel(application) {
companion object {
private var note: Notes = Notes(0, "", "test title", "test ontent")
}
fun getNote(uuid: Long?): Notes {
val job = async(CommonPool) {
getNoteAsyncTask(notesDatabase).execute(uuid)
}
runBlocking { job.await() }
return note
}
class getNoteAsyncTask(database: NotesDatabase) : AsyncTask<Long, Unit, Unit>() {
private val db: NotesDatabase = database
override fun doInBackground(vararg params: Long?) {
note = db.notesDataDao().getNote(params[0])
}
}
}
If I pass an intent to get a Note object from the database with a uuid and set that received data in titleEditText and contentEditText, the data set in the Note was from previous intent invoked when we clicked on the Note item in RecyclerView. On clicking the Note item for the first time, I get the default value which I have set "test title" and "test content".
Aforementioned is the behavior most of the time. Sometimes the data set in titleEditText and contentEditText is of the correct Note object.
Can someone please tell me what I have done wrong? How can I correct my apps behavior?
Unfortunately, there is a big mistake in how you use a view model to provide a data to your view(AddNoteActivity).
Basically, your view never has a chance to wait for the data to be fetched as it always receives a default value. This happens because the AsyncTask runs on its own thread pool so the coroutine completes immediately and returns a default value.
You should consider using LiveData to post a new object to your view and refactor your view model.
So, you need to make a query to the database synchronous and observe changes to a note rather than have a getter for it. Of course, in a real life scenario it might be a good idea to have different kind of states to be able to show a spinner while a user is waiting. But this is another big question. So to keep things simple consider changing your view model to something like that:
class NoteViewModel(private val database: NotesDatabase) : ViewModel { // you do not need an application class here
private val _notes = MutableLiveData<Notes>()
val notes: LiveData<Notes> = _notes
fun loadNotes(uuid: Long) {
launch(CommonPool) {
val notes = database.notesDataDao().getNote(uuid)
_notes.setValue(notes)
}
}
}
Then, you can observe changes to the note field in your activity.
class AddNoteActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
val noteViewModel = ViewModelProviders.of(this).get(NoteViewModel::class.java)
noteViewModel.notes.observe(this, Observer {
title.text = it.title
content.text = it.note
})
}
}
Also you need to use a ViewModelProvider.Factory to create your view model and properly inject dependencies into it. Try to avoid having a context there as it makes it much harder to test.
Background
Some articles claim that Rx can replace AsyncTask and AsyncTaskLoader.
Seeing that Rx usually makes code shorter, I tried to dive into various samples and ideas of how it works.
The problem
All of the samples and articles I've found, including Github repos, don't seem to really replace AsyncTask and AsyncTaskLoader well:
Can't find how to cancel a task or multiple ones (including interrupting a thread). This is especially important for AsyncTask, which is usually used for RecyclerView and ListView. This can also be done on AsyncTaskLoader, but requires a bit more work than just call "cancel". For AsyncTask, it also provides publishing of a progress, and that's another thing I can't find an example of in Rx.
Can't find how to avoid unsubscribing manually while avoiding memory leaks, which quite ruins the whole point of using Rx, as it's supposed to be shorter. In some samples, there are even more callbacks than normal code has.
What I've tried
Here are some links I've read about Rx:
https://stablekernel.com/replace-asynctask-and-asynctaskloader-with-rx-observable-rxjava-android-patterns/ - this actually provides a cool way to handle AsyncTaskLoader, as it caches the result. However, the samples don't work at all (including building them), as they are very old.
https://code.tutsplus.com/tutorials/getting-started-with-rxjava-20-for-android--cms-28345 , https://code.tutsplus.com/tutorials/reactive-programming-operators-in-rxjava-20--cms-28396 , https://code.tutsplus.com/tutorials/rxjava-for-android-apps-introducing-rxbinding-and-rxlifecycle--cms-28565 - all those talk mostly about RxJava, and not about replacement of AsyncTask or AsyncTaskLoader.
https://github.com/L4Digital/RxLoader - requires unsubscribing, even though it doesn't say it needs it.
The question
How can I use Rx as replacement for AsyncTask and AsyncTaskLoader?
For example, how would I replace this tiny AsyncTaskLoader sample code with Rx equivalent:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportLoaderManager.initLoader(1, Bundle.EMPTY, object : LoaderCallbacks<Int> {
override fun onCreateLoader(id: Int, args: Bundle): Loader<Int> {
Log.d("AppLog", "onCreateLoader")
return MyLoader(this#MainActivity)
}
override fun onLoadFinished(loader: Loader<Int>, data: Int?) {
Log.d("AppLog", "done:" + data!!)
}
override fun onLoaderReset(loader: Loader<Int>) {}
})
}
private class MyLoader(context: Context) : AsyncTaskLoader<Int>(context) {
override fun onStartLoading() {
super.onStartLoading()
forceLoad()
}
override fun loadInBackground(): Int {
Log.d("AppLog", "loadInBackground")
try {
Thread.sleep(10000)
} catch (e: InterruptedException) {
e.printStackTrace()
}
return 123
}
}
}
For AsyncTask, usually we set it per ViewHolder, as a field there, and we cancel it upon onBindViewHolder, and when activity/fragment gets destroyed (go over all those that still pending or running).
Something like this (sample made as short as possible, of course it should be changed depending on needs) :
class AsyncTaskActivity : AppCompatActivity() {
internal val mTasks = HashSet<AsyncTask<Void, Int, Void>>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_async_task)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
recyclerView.adapter = object : Adapter<MyViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(LayoutInflater.from(this#AsyncTaskActivity).inflate(android.R.layout.simple_list_item_1, parent, false))
}
#SuppressLint("StaticFieldLeak")
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.tv.text = "loading..."
if (holder.task != null) {
holder.task!!.cancel(true)
mTasks.remove(holder.task!!)
}
holder.task = object : AsyncTask<Void, Int, Void>() {
override fun doInBackground(vararg voids: Void): Void? {
for (i in 0..100)
try {
Thread.sleep(5)
publishProgress(i)
} catch (e: InterruptedException) {
e.printStackTrace()
}
return null
}
override fun onProgressUpdate(vararg values: Int?) {
super.onProgressUpdate(*values)
holder.tv.text = "progress:" + values[0]
}
override fun onPostExecute(aVoid: Void?) {
super.onPostExecute(aVoid)
mTasks.remove(holder.task)
holder.tv.text = "done:" + position
holder.task = null
}
}.execute()
}
override fun getItemCount(): Int {
return 1000
}
}
}
override fun onDestroy() {
super.onDestroy()
for (task in mTasks)
task.cancel(true)
}
private class MyViewHolder(itemView: View) : ViewHolder(itemView) {
internal var tv: TextView
internal var task: AsyncTask<Void, Int, Void>? = null
init {
tv = itemView.findViewById(android.R.id.text1)
}
}
}
Let me tell you in advance that my experience in RxJava isn't as some would call it advance. I just know enough to replace AsyncTask using RxJava + RxAndroid. For example, let's convert your AsyncTask code to RxJava2 equivalent using Observable, which is arguably the most popular stream type.
Observable
.create <Int> { emitter ->
try {
for (i in 0..100) {
Thread.sleep(5)
emitter.onNext(i)
}
} catch (e: InterruptedException) {
emitter.onError(e)
}
emitter.onComplete()
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ i ->
holder.tv.text = "progress:" + i
}, { e ->
e.printStackTrace()
}, {
holder.tv.text = "done:" + position
})
Now let's go through the code line-by-line:
create lets you customize emitter behavior. Note that this isn't the only method to create the streams, there are also just, fromArray, fromList, defer, etc. We can discuss more of that if you are still interested.
subscribeOn(Schedulers.io()) tells RxJava that #1 will be executed in I/O thread. If this is a background operation, Schedulers.newThread() also works (at least to my knowledge it does).
observeOn(AndroidSchedulers.mainThread()) tells RxJava that #4 will be executed in Android's main thread. This is also where RxAndroid fills the gap since RxJava doesn't have any reference of Android.
subscribe()
onNext(i : Int) - equivalent to publishProgress(). In newer RxJava, emitted value cannot be null.
onError(e : Throwable) - error handling that is long glorified by RxJava's programmers. In newer RxJava, onError() must be provided, doesn't matter if it's empty.
onComplete() - equivalent to onPostExecute().
Again, there might be a lot of misinformation with my answer. I expect someone with better understanding to correct it or provide better answer that this one.
It seems there is no way to implement AsyncTask functionality with RxJava in shorter or more simple form. With RxJava you simplify task chaining, so if you have only one task AsyncTask can be better solution, but when you have several consecutive tasks to do - RxJava's interface is more convenient. So instead of, for example, program onCancelled for each task you should rather implement some common logic.