This is a sample app crafted for fun and learning. It uses the api from flickr which is documented here FlickApi.
The app uses the MVVM architecture implemented using Android Arch Components
- Well separation of concerns for Business Logic (in Repositories), Presentation Logic (in ViewModel) and Presentation(Activities and Xml)
- In-turn increases the testability of individual components.
NOTE For the app of this scale, MVVM might not be needed in general but it really good to use it in android apps because a lot of complications that arise due to configuration changes and async work is automatically taken care of by ViewModels.
Other third party libs include
- Retrofit - For making network calls
- Hamcrest - For advanced matching in unit tests
- Mockito - For mocking
The app is composed of 2 main screens
Displays the icon and name of the app and navigates to HomeActivity after 1.5 seconds. The app logic can be found in SplashViewModel
Displays the list of recent photos by default and displays the search results when user enters a filter.
The HomeViewModel exposes 3 LiveDatas using which the state of this activity's UI is developed.
- ResultsStateLiveData - Used to show the progress bar and error
- AllResultsLiveData - Contains the latest results need to be shown to the user
- LoadMoreStateLiveData - Used to show the horizontal progress bar when more items are being fetched
HomeActivity sends user actions to HomeViewModel using simple function calls like:
fetchPhotos("kittens")
searchTextChanged("Street triple 765 rs")
loadNextPage()
refresh()
Right now, no dependency injection framework like Dagger is being used and hence the small dependency graph need to be created manually which can be seen in FlickyApplication class
ApiResponse [link]
-
Used to handle the network response and convert it into a meaningful error message in case of any kind of error (Network error, Flick error, parsing Error).
-
As of now this is tightly coupled with GetRecentResponse model. Eventually it should be made generic as the app scales.
GetRecentPhotosTask [link]
This is used to perform the network task of fetching recent photos or fetching the search results for a given query.
- It spits out network resource (PhotosResponse in our case) states in the form of LiveData using Resource class
- It is tightly coupled to PhotosResponse as of now which should eventually be generalised
- This class can be made an abstract class which can be a baseline for any kind of network request which needs to be backed by a LiveData.
Resource [link]
This is a great utility which I prefer to use in apps to represent a network resource's state. This really helps to let any body know of the network resouces' state in the app including the UI.
Nothing fancy going on here but in the future this can be a major hub for images which supports caching results for query in a local db using Room, searching local db if network is not available etc.
No UI tests as of now
Local unit tests can be found for HomeViewModel
, SplashViewModel
, ApiResponse
and Resource
GCache is a custom made image caching library which uses Disk and Memory caching together. Its built expicitly for Flickry and is fairly simple to use.
As simple as that
GCache.using(context).load(url).into(imageView);
- This library maintains 50 recent bitmaps in memory and upto 500 bitmaps on disk.
- It is bounded by number of images but eventually it should be bounded by a max memory limit for disk and in-memory.
GCache [link]
Its the main class of the library.
- Initialises the BitmapLruCache which can either be a MemoryOnlyLruCache or DiskBackedLruCache. Right now its
DiskBackedLruCache
always. - Also initialises the required executors to execute image loading tasks and process loading commands.
- The load command:
@UiThread static void load(GCacheRequestBuilder requestBuilder) { // Clear existing image requestBuilder.getImageViewWeakReference().get().setImageResource(0); // If there is an existing task for this image view, cancel that task if (sImageViewTasks.containsKey(requestBuilder.getImageViewWeakReference().get())) { sImageViewTasks.get(requestBuilder.getImageViewWeakReference().get()).cancel(); sImageViewTasks.remove(requestBuilder.getImageViewWeakReference().get()); } // Take it off the main thread, as sLruCache's methods are synchronized sLoadExecutor.execute(() -> { // If bitmap is present in cache, no need to do fancy stuff if (sLruCache.get(requestBuilder.getUrl()) != null) { showImage(requestBuilder,sLruCache.get(requestBuilder.getUrl())); return; } GCacheTask task = new GCacheTask(requestBuilder, sLruCache, (imageView) -> { sMainHandler.post(() -> { // To ensure sImageViewTasks is handled by a single thread only if (imageView != null && sImageViewTasks.containsKey(imageView)) { sImageViewTasks.remove(imageView); } }); }); sImageViewTasks.put(requestBuilder.getImageViewWeakReference().get(), task); sExecutor.execute(task); }); }
GCacheRequestBuilder [link]
- Represents a loading request made by client
- Contains a weak reference to the ImageView provided by
into()
call - Calls
GCache.load(this)
Place where the actual network request happens
DiskBackedLruCache [link]
-
Maintains a sub LRUCache for in memory caching of upto 50 recent bitmaps
-
Maintains another LRUCache(
mDiskBoundCache
) of file names of upto 500 bitmapsNOTE The file names LRUCache's(
mDiskBoundCache
) item removal is listened by DiskBackedLruCache to delete the coressponding file on disk -
Initialises
mDiskBoundCache
by reading the files in the directory -
Put operation updates memory cache, writes to disk and then updates the
mDiskBoundCache
@Override public synchronized void put(String key, Bitmap bitmap) { mLruCache.put(key, bitmap); writeToDisk(key, bitmap); mDiskBoundCache.put(key.hashCode(), 1); }
-
get operation first gets from memory cache, if not present then gets it from disk and updates in memory cache before returning
@Override public synchronized Bitmap get(String key) { if(mLruCache.get(key) != null) { return mLruCache.get(key); } // PAGE FAULT -> Get it from disk if (mDiskBoundCache.get(key.hashCode()) != -1) { // Update in-memory cache Bitmap b = readFromDisk(key.hashCode()); mLruCache.put(key, b); return b; } return null; }