May 14 | 8:20 AM |
Mark M. | has entered the room |
May 14 | 8:25 AM |
Mark M. | turned on guest access |
May 14 | 8:30 AM |
Kai H. | has entered the room |
Kai H. |
Hi Mark
|
terence | has entered the room |
Mark M. |
hello, Kai!
|
terence |
morning mark
|
Mark M. |
and hello terence!
|
Kai H. |
hi
|
Mark M. |
Kai, I think you got in first, so... how can I help you today?
|
Kai H. |
I'd give Terence precedence if he has a question
|
Kai H. |
I feel like I get a lot of time as I am present in most office hours :D
|
Mark M. |
OK, sounds fine
|
terence |
thanks
|
Mark M. |
Terence: hi again! how can I help you today?
|
terence |
I’m trying to understand how caches are typically used in modern apps. Suppose I have a list of items retrieved from a server. I also have a cache mechanism and progress indicator. When is the cached utilized? Is it before items are retrieved from the server? If a progress indicator is usually displayed prior to a response from a server is it still displayed?
|
Mark M. |
are you referring to an on-disk cache or an in-memory cache? or both?
|
terence |
would the answer be different? on-disk
|
Mark M. |
an in-memory cache is usually purely a cache
|
May 14 | 8:35 AM |
Mark M. |
once you start persisting data to disk, the debate starts to creep in: is this really a cache, or is this more of a primary data source?
|
Mark M. |
if you truly mean a cache, and that the server is the system of record at all times, then the next question is: does your app think that the cache is reasonably up to date at the time you are trying to show this list?
|
Mark M. |
it also depends a lot on the nature of the on-disk cache -- caching OkHttp/Retrofit responses is different than having your own Room tables, for example
|
Mark M. |
the simplest caches are transparent to your app code, such as the OkHttp/Retrofit response caching
|
terence |
by cache I mean storage used to restore an app if it is destroyed involuntarily so I was thinking sharedpreferences or a room database
|
Mark M. |
OK
|
Mark M. |
if you think the cache is likely to be fairly up to date, a common approach is to show the cached results, while doing some background work to ensure the cache is updated (and also update the UI with any changes)
|
Mark M. |
the algorithm for "fairly up to date" will depend partly on the age of the cached data and partly on the frequency of data changes on the server
|
Mark M. |
if the user's data on the server doesn't change very often, the cache is more useful
|
May 14 | 8:40 AM |
Mark M. |
in terms of a progress indicator, I'd consider one during the disk/database I/O
|
May 14 | 8:40 AM |
terence |
so does the user see cache and then a progressbar followed by network data?
|
Mark M. |
that is up to your UI designers
|
Mark M. |
I would not use a blocking progress indicator, like a dialog, if the cached data is useful
|
Mark M. |
but, if you want some sort of indicator that shows that a sync is ongoing, you might find a way to do that
|
terence |
why would you place the progress indicator during the database io, isnt fetching from the network usually a longer task
|
Mark M. |
yes, but there is nothing to show the user before the database I/O completes
|
Mark M. |
if you are very confident that the database I/O will be quick, you could skip the progress indicator
|
Mark M. |
however, phones vary quite a bit in performance, and I do not know how complex your database I/O would be
|
terence |
thanks, i will ask a follow up but let kai go
|
Mark M. |
OK, sounds good -- I'll come back to you in a bit
|
Mark M. |
Kai: your turn!
|
Kai H. |
I have implemented a list with RecyclerView and an Adapter and the such, following your example from "Jetpack" mostly.
|
Kai H. |
I show an InputDialog if one of the list items is clicked.
|
Kai H. |
And wondered where to place the Dialog and how to get and store the result
|
Mark M. |
I am uncertain what you mean by "where to place the Dialog"
|
Kai H. |
The way I do it right now is that I initialize my Adapter with a callback and then start the Dialog from there.
|
Mark M. |
ah, OK
|
May 14 | 8:45 AM |
Mark M. |
is this Dialog managed by a DialogFragment?
|
Kai H. |
The callback is from the Fragment that holds the RecyclerView and Adapter.
|
Kai H. |
Yes, it's a DialogFragment.
|
Kai H. |
Sorry, I am actually wrong :D I just looked it up again
|
Mark M. |
that's too bad, because showing the dialog from that callback seems reasonable, based on your description :-)
|
Kai H. |
I start the DalogFragment from another Fragment (the one that is shown prior to starting the Dialog).
|
Mark M. |
OK, that sounded like what you wrote before, though
|
Kai H. |
And it started from the Adapter via a Callback that I passed into the Adapter constructor
|
Kai H. |
I thought I actually have the Dialog _inside_ my Adapter, which would be rather strange :D
|
Mark M. |
if you are showing the DialogFragment from the hosting Fragment, via a callback supplied to the RecyclerView.Adapter, that sounds fine
|
Mark M. |
I would not have the Adapter itself show the DialogFragment, but having it invoke a callback on the click(?) event is fine
|
Kai H. |
It feels a bit... messy for some reason. All over the place
|
Kai H. |
Yes, it's a click event. I also pass in a Listener for a drag event, to be able to drag list items via a drag handler.
|
May 14 | 8:50 AM |
Mark M. |
well, only the Adapter and the ViewHolder know about the UI events on the RecyclerView items, so they have to be involved somehow
|
Mark M. |
at the same time, only the Fragment knows what is to be done when the events occur
|
Mark M. |
so, you need to ripple the UI events up to the Fragment by one means or another
|
Kai H. |
I also pass in the ViewModel of the parent Fragment, because there are some fields I need from it.
|
Kai H. |
Ok.
|
Mark M. |
a callback is a reasonable approach
|
Mark M. |
my current client project uses RxJava PublishSubject objects, which isn't my preferred approach but works
|
Kai H. |
Do you prefer Callbacks?
|
Mark M. |
a Kotlin project could use a BroadcastChannel from coroutines
|
Mark M. |
for a Java project, and in this situation, callbacks are fine
|
Mark M. |
a Fragment and a RecyclerView are tightly coupled by their very nature
|
Mark M. |
contrast that with a Fragment and its ViewModel, which may have distinctly different lifecycles
|
Mark M. |
but, one way or another, I tend to put navigational thinks like FragmentTransactions, showing DialogFragments, startActivity(), on the activities/fragments, not on UI elements like a RecyclerView
|
Mark M. |
(er, navigational *things*)
|
Kai H. |
Yes, that feels better. I also do that (it seems ;-) ).
|
Mark M. |
let me take another question from Terence, and I'll come back to you in a bit
|
Mark M. |
Terence: back to you! do you have another question?
|
May 14 | 8:55 AM |
terence |
Suppose I want my list of results to be up to date so I fetch from the network during onResume(). Is there a way to prevent making the network request on rotation and continue to request new data during onResume()?
|
Mark M. |
ideally, in modern Android architectures, your network I/O is being managed by a ViewModel, so it survives configuration changes and knows when that I/O is needed
|
Mark M. |
whether your ViewModel *does* the I/O, or if that is handled by a singleton repository, or by a data source object managed by a singleton repository, is a separate matter
|
terence |
I am using a presenter
|
Mark M. |
one way or another, you want the I/O to be managed by something with an appropriate lifecycle, and one that is not destroyed/recreated due to a configuration change
|
Mark M. |
in the Jetpack, that would be a ViewModel
|
Mark M. |
other frameworks hopefully have equivalent options
|
terence |
I am using a presenter as part of MVP; typically I make my io requests in my presenter that is heavily tied to the activity lifecycle
|
Mark M. |
I would get the actual I/O out of there, perhaps into a repository object that survives configuration changes
|
Mark M. |
your presenter would then ask the repository object for the data, and the repository object would know the state of the cache, whether network I/O is needed in this case, etc.
|
May 14 | 9:00 AM |
terence |
should this repository be a singleton to outlive the activity lifecylce?
|
Mark M. |
yes, ideally managed by some sort of dependency inversion framework (Dagger, Koin, etc.)
|
Mark M. |
so that in testing you can swap in a mock or fake repository
|
terence |
im confused how this would work, do I query the repository in onResume()?
|
Mark M. |
I cannot really answer that, as I do not know enough of your code
|
Mark M. |
I show the use of repositories in a few spots in *Elements of Android Jetpack*
|
Mark M. |
and *Exploring Android* sets up an app that uses repositories as well
|
terence |
yes, but it uses viewmodels and i am not using it
|
Mark M. |
sorry, but I have not worked on an MVP Android project and cannot really comment on how that architecture works with respect to lifecycle methods
|
Mark M. |
this is one of the reasons why I try to focus more on APIs than architecture in my books
|
Mark M. |
however, it does limit my ability to help with your sort of question, and for that I apologize
|
Mark M. |
let me take another question from Kai, and I will return to you shortly
|
May 14 | 9:05 AM |
terence |
sure
|
Mark M. |
Kai: back to you! do you have another question?
|
Kai H. |
Thanks
|
Kai H. |
Do you prefer to group files by function or by type? Like "all files concerning one piece of functionality go there" vs "all Fragments go there, all ViewModels here, all..."
|
Mark M. |
if by "function" you mean "subject matter", I tend to go that route
|
Mark M. |
depending on the app, that has a tendency to map to logical screens
|
Kai H. |
function = funcionality, some aspect of the app like "show a list of items" or "create a new item"
|
Mark M. |
yeah, OK, then that's what I tend to see
|
Mark M. |
you can see a bit of that in the to-do app in *Exploring Android*
|
Kai H. |
Doesn't that create problems if/when you have a hierarchy of some sort? Say you have a base fragment with some functionality that your other fragments inherit from?
|
Kai H. |
hierarchy = inheritance hierarchy
|
May 14 | 9:10 AM |
Mark M. |
for those common elements, I've seen a few patterns, from "dump it in the top-level package" to "have a utility or UI package"
|
Mark M. |
the original reason for "package by functionality" stemmed from Java's package-private visibility option, as closely-coupled items (fragment and a viewmodel, for example) could rely on package-private stuff that other packages could not access
|
Mark M. |
Kotlin doesn't really have package-private, so that particular bit of rationale falls away
|
Kai H. |
The app I maintain goes very heavy on the "group by type", but I often feel it would be nicer to have everything regarding a feature in one place instead of having it spread out into the respective packages.
|
Mark M. |
I agree, but wholesale refactorings can be a pain
|
Kai H. |
I guess so. That should be easier in this case.
|
Kai H. |
There seems to be no real answer to this one besides: It depends.
|
Mark M. |
sorry, I meant switching organization schemes from what you have to what you want
|
Mark M. |
I'
|
Mark M. |
I'd phrase it as "there is no canonical right answer"
|
May 14 | 9:15 AM |
Kai H. |
Oh, ok. I thought about switchting from Loaders to ViewModels for example, that could also be easier if all the components that need to be touched are in one place already.
|
Kai H. |
On the other hand, it might make it more difficult, as it might mean "refactor everything at once or nothing at all"
|
Kai H. |
Instead of just refactoring one piece and then another
|
Mark M. |
it's tough for me to comment, since I don't know your codebase
|
Mark M. |
let me switch back to Terence, and I'll try to return to you once more before the chat is over
|
Mark M. |
Terence: do you have another question?
|
terence |
if I used a viewmodel like you suggested, how does it know to serve a result from a database versus a server? Is it correlated to its view's lifecycle>
|
Mark M. |
well, on its own, it might not -- it depends on how much work you're trying to cram into the viewmodel
|
Mark M. |
if you are employing repositories, they definitely need to know the state of their caches, the state of outstanding network I/O, etc.
|
Mark M. |
in that case, the viewmodel is mostly a container for a chunk of results and coordinator for requests to the repositories
|
terence |
so who fetches from the repo and makes that decision?
|
Mark M. |
the viewmodel asks the repo for the data, but the viewmodel should neither know nor care how that data is obtained
|
Mark M. |
whether the repository is using an in-memory cache, on-disk cache, network call, or random number generator is outside the viewmodel's area of responsibility
|
Mark M. |
key to that is having a reactive API from the repository, so the repository can deliver results asynchronously, and perhaps repeatedly (e.g., cache results vs. final network call results)
|
May 14 | 9:20 AM |
Mark M. |
so, for example, you mentioned Room
|
Mark M. |
one approach to using Room for an on-disk cache is to have the repository return the results of a call to a Room DAO, where those results are a LiveData or Observable or Flow
|
Mark M. |
Room will then not only do its query when asked, but it will deliver fresh updates when the data changes
|
Mark M. |
the repo can then kick off the network I/O, which updates Room
|
Mark M. |
Room, in turn, emits fresh query results via the LiveData/Observable/Flow
|
Mark M. |
all the viewmodel needs to is tell the repository "give me my data"
|
terence |
what book and chapter contains an example using both room and networked results? I read a chapter that just described using room with livedata, no network results
|
Mark M. |
IIRC, the Weather sample in the *Elements of Kotlin Coroutines* appendix uses both Room and Retrofit
|
terence |
I still dont understand who makes a decision to get database or network results
|
Mark M. |
the repository would, based on the state of the cache
|
terence |
and how that decision is made i mean
|
Mark M. |
I can't answer that, as it depends a *lot* on your data model and your server
|
terence |
in your example, we have a list of data persisted to room. when the cache is looked at, what criteria would trigger a room fetch versus a new network request?
|
Mark M. |
for example, let's suppose that we are writing an app that is displaying financial data, such as stock prices or currency conversion rates
|
May 14 | 9:25 AM |
Mark M. |
in the case of financial data, either it changes very rapidly (when markets are open) or not at all (when markets are closed)
|
Mark M. |
so, the repository can know those business rules
|
Mark M. |
when markets are closed, if the cache is up to date with respect to the last closing time, it can return the cached results without bothering with a server call
|
terence |
oh, I see, so you would have logic in your repository to decide whether to use the network DAO or room DAO
|
Mark M. |
when markets are open, it might return the cached data but also make the network call
|
Mark M. |
right
|
Mark M. |
and the details of that logic depend on your particular app and its data sources, so there is no universal solution
|
terence |
View paste
|
Mark M. |
um, off the top of my head, other than the !!, what you have seems like it should have been OK
|
Mark M. |
that's the sort of thing I'd have to poke around in the IDE for myself
|
terence |
ok
|
terence |
thanks mark!
|
terence |
bye
|
Mark M. |
see you later!
|
Mark M. |
Kai: do you have another (quick) question?
|
Kai H. |
Not quick enough I guess ;-)
|
Kai H. |
I will have to wait for saturday
|
Mark M. |
yeah, there's always another chat! :-)
|
Kai H. |
Are you immortal?
|
May 14 | 9:30 AM |
Mark M. |
not that I am aware of
|
Kai H. |
;-)
|
Kai H. |
Have a good time
|
Mark M. |
you too!
|
Mark M. |
the next chat is Thursday at 4pm US Eastern
|
Mark M. |
have a pleasant day, and stay healthy!
|
Kai H. | has left the room |
terence | has left the room |
Mark M. | turned off guest access |