I am new to android development and trying to build the UI for my application.
The app integrates with REST backend which accepts a search query and a list of items as response.
interface RetrofitEndpoint {
#GET("paged/list/endpoint")
Call<PagedList<Object>> getPagedList(#Query("query") String query, #Query("pageSize") int pageSize, Query("pageOffset") int pageOffset);
}
The UI displays one item at a time to the user.
I am loading the list into a recyclerview
public class SomeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<Object> list;
// .. other overridden members
public void setList(List<Object> list) {
this.list = list;
notifyDataSetChanged();
}
public void addAll(List<Object> newList) {
int lastIndex = list.size() - 1;
list.addAll(newList);
notifyItemRangeInserted(lastIndex, newList.size());
}
}
The part that I am not able to figure out is how do I load more data when I reach the end(or before to avoid latency) of my recyclerview, is there any library/API that does this?
For paged list to work properly it requires bit more stuff in your app.This implementation uses view model, live data and room persistence so it works offline.
your build.gradle:
// ViewModel
def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
def paging_version = "2.1.2"
implementation "androidx.paging:paging-runtime:$paging_version"
Retrofit api:
interface RetrofitEndpoint {
#GET("paged/list/endpoint")
Call<List<YourObject>> getYourObjectList(#Query("query") String query, #Query("pageSize") int pageSize, Query("pageOffset") int pageOffset);
}
YourObject:
#Entity(tableName = "your_object")
public class YourObject implements Serializable {
#PrimaryKey(autoGenerate = true)
private int db_id;
...
Dao:
#Dao
public interface YourObjectDao{
/**
* Get your objects from the table.
* -------------------------------
* We get update every time the database update.
*
*
* #return your object from the table
*/
#Insert
void insert(YourObject yourObject);
#Insert
void insertList(List<YourObject> yourObjectList);
#Query("SELECT * FROM your_object")
DataSource.Factory<Integer, YourObject> getAllResults();
#Query("DELETE FROM your_object")
void deleteAll();
}
Database:
#androidx.room.Database(entities = {YourObject.class}, version = 1)
public abstract class Database extends RoomDatabase {
private static Database instance;
public abstract YourObjectDao get_your_object_dao();
public static synchronized Database getInstance(Context context) {
if (instance == null) {
instance = Room.databaseBuilder(context.getApplicationContext(),
Database.class, DATABASE_NAME)
.fallbackToDestructiveMigration()
.addCallback(roomCallback)
.build();
}
return instance;
}
}
YourObjectBoundaryCallback:
public class YourObjectBoundaryCallback extends PagedList.BoundaryCallback<YourObject> {
private AppExecutors executors;
private Database database;
private YourObjectDao dao;
private Integer page_number;
public YourObjectBoundaryCallback (Application application, AppExecutors executors) {
//super();
this.executors = executors;
database = Database.getInstance(application);
dao = database.get_your_object_dao();
page_number=1;
}
#Override
public void onZeroItemsLoaded() {
super.onZeroItemsLoaded();
Log.d("log", "yourObjects onzeroitemsloaded");
fetchYourObjects(page_number);
}
#Override
public void onItemAtFrontLoaded(#NonNull YourObject itemAtFront) {
super.onItemAtFrontLoaded(itemAtFront);
Log.d("log", "yourObjects onItemAtFrontLoaded");
}
#Override
public void onItemAtEndLoaded(#NonNull YourObject itemAtEnd) {
super.onItemAtEndLoaded(itemAtEnd);
Log.d("log", "yourObjects onItemAtEndLoaded");
page_number=page_number+1;
fetchYourObjects(page_number);
}
public void fetchYourObjects(int pageNumber) {
RetrofitApi retrofitApi = RetrofitInstance.getRetrofitEndpoint();
Call<List<YourObject>> call = retrofitApi.getYourObjectList(query, pageSize,pageNumber);
call.enqueue(new Callback<List<YourObject>>() {
#Override
public void onResponse(Call<List<YourObject>> call, Response<List<YourObject>> response) {
if (!response.isSuccessful()) {
Log.d("log", "YourObjects Response unsuccesful: " + response.code());
return;
}
Log.d("log", "YourObjects Response ok: " + response.code());
List<YourObject> yourObjectsList = response.body();
insertListToDb(yourObjectsList );
}
#Override
public void onFailure(Call<List<YourObject>> call, Throwable t) {
Log.d("log", "yourObjects onFailure: " + t.getMessage());
}
});
}
public void insertListToDb(List<YourObject> list) {
Runnable runnable = () -> {
dao.insertList(list);
};
Runnable diskRunnable = () -> database.runInTransaction(runnable);
executors.diskIO().execute(diskRunnable);
}
}
YourObjects Repository:
public class YourObjectsRepository {
private LiveData<PagedList<YourObject>> yourObjectsPagedList;
private YourObjectBoundaryCallback yourObjectsBoundaryCallback;
private AppExecutors executors;
public YourObjectsRepository (Application application, AppExecutors executors) {
this.executors = executors;
Database database = Database.getInstance(application);
YourObjectDao dao = database.get_your_object_dao();
yourObjectsBoundaryCallback= new YourObjectBoundaryCallback (application, executors);
createYourObjectsPagedList(dao );
}
//this is configuration for your paged list, adjust per your requirements
private PagedList.Config getPagedListConfig(){
return (new PagedList.Config.Builder())
.setEnablePlaceholders(false)
.setPrefetchDistance(40)
.setInitialLoadSizeHint(60)
.setPageSize(20).build();
}
private void createYourObjectsPagedList(YourObjectDao dao){
yourObjectsPagedList= new LivePagedListBuilder<>(dao.getAllResults(), getPagedListConfig())
.setBoundaryCallback(yourObjectsBoundaryCallback).setFetchExecutor(executors.networkIO())
.build();
}
public LiveData<PagedList<YourObject>> getYourObjectsPagedList() {
return yourObjectsPagedList;
}
}
YourObjectsViewModel:
public class YourObjectsViewModel extends AndroidViewModel {
private YourObjectsRepository repo;
public YourObjectsViewModel (#NonNull Application application) {
super(application);
AppExecutors executors = new AppExecutors();
repo= new YourObjectsRepository (application, executors);
}
public LiveData<PagedList<YourObject>> getYourObjectsPagedList() {
return repo.getYourObjectsPagedList();
}
}
AppExecutors:
public class AppExecutors {
private final Executor diskIO;
private final Executor networkIO;
private final Executor mainThread;
private final Executor others;
private final Executor paging;
public AppExecutors(Executor diskIO, Executor networkIO, Executor mainThread, Executor others, Executor paging) {
this.diskIO = diskIO;
this.networkIO = networkIO;
this.mainThread = mainThread;
this.others = others;
this.paging = paging;
}
public AppExecutors() {
this(Executors.newSingleThreadExecutor(), Executors.newFixedThreadPool(3),
new MainThreadExecutor(), Executors.newSingleThreadExecutor(),
Executors.newFixedThreadPool(4));
}
public Executor diskIO() {
return diskIO;
}
public Executor networkIO() {
return networkIO;
}
public Executor mainThread() {
return mainThread;
}
public Executor others() {
return others;
}
public Executor paging() {
return paging;
}
private static class MainThreadExecutor implements Executor {
private Handler mainThreadHandler = new Handler(Looper.getMainLooper());
#Override
public void execute(#NonNull Runnable command) {
mainThreadHandler.post(command);
}
}
}
in your activity / fragment:
yourObjectsViewModel = new ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory.getInstance(getActivity().getApplication())).get(YourObjectsViewModel.class);
yourObjectsViewModel.getYourObjectPagedList().observe(getViewLifecycleOwner(), new Observer<PagedList<TopRatedMovie>>() {
#Override
public void onChanged(PagedList<YourObject> results) {
Log.d("log", " onChanged list size: " + results.size());
yourAdapter.submitList(results);
}
});
In your adapter:
public class YourPagedListAdapter extends PagedListAdapter<YourObject,
RecyclerView.ViewHolder> {
If u have any questions feel free to ask.
you could add onScrollStateChanged listener to your RecyclerView to detect the current position of your RecyclerView, then add your logic to fetch in you desired certain condition
Related
I have a room DB class that creates 3 user objects -
#Database(entities = {User.class}, version = 1, exportSchema = false)
public abstract class UserDatabase extends RoomDatabase {
private static UserDatabase instance;
public abstract UserDao userDao();
public static synchronized UserDatabase getInstance(Context context) {
Log.d("inside observe - ", "inside database");
if (instance == null) {
instance = Room.databaseBuilder(context.getApplicationContext(), UserDatabase.class, "user_database").fallbackToDestructiveMigration().addCallback(roomUserCallback).build();
}
return instance;
}
private static RoomDatabase.Callback roomUserCallback = new RoomDatabase.Callback() {
#Override
public void onCreate(#NonNull SupportSQLiteDatabase db) {
super.onCreate(db);
new PopulateDbAsyncTask(instance).execute();
}
};
//TODO - delete this in the future. This is just for populating.
private static class PopulateDbAsyncTask extends AsyncTask<Void, Void, Void> {
static final String URL = "https://www.shortlist.com/media/images/2019/05/40-favourite-songs-of-famous-people-28-1556672663-9rFo-column-width-inline.jpg";
static final String URL2 = "https://img-s-msn-com.akamaized.net/tenant/amp/entityid/BBR9VUw.img?h=416&w=624&m=6&q=60&u=t&o=f&l=f&x=2232&y=979";
static final String URL3 = "https://dz9yg0snnohlc.cloudfront.net/new-what-famous-people-with-depression-have-said-about-the-condition-1.jpg";
private UserDao userDao;
private PopulateDbAsyncTask(UserDatabase db) {
userDao = db.userDao();
}
#Override
protected Void doInBackground(Void... voids) {
userDao.insert(new User(URL, "Barak Obama1", "/#BarakObama1"));
userDao.insert(new User(URL2, "Barak Obama2", "/#BarakObama2"));
userDao.insert(new User(URL3, "Barak Obama3", "/#BarakObama3"));
return null;
}
}
}
I am using viewmodel in order to fetch the users as LiveData.
For some reason, at the first time I install my app, I get one extra "barak obama1" user created, and immedeatly after than all 3 "normal" users by order - barak obama3, 2 and 1.
Here is my MainActivity -
private ArrayList<User> usersList;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
fetchUserList();
}
private void fetchUserList() {
userViewModel = ViewModelProviders.of(this).get(UserViewModel.class);
final Observer<List<User>> userObserver = users -> {
Log.d("inside observe - ", "inside main activity, list size - " + users.size());
usersList = (ArrayList) users;
initViewsAndListeners();
addCards();
};
userViewModel.getAllUsers().observe(this, userObserver);
}
private void addCards(){
TinderCardView tinderCardView;
for (int i = 0; i < usersList.size(); i++) {
tinderCardView = new TinderCardView(this);
tinderCardView.bind(usersList.get(i));
Log.d("inside observe - ", "inside main activity, user value - " + usersList.get(i).getUsername());
tinderStackLayout.addCard(tinderCardView);
Log.d("addCardCalled - ", "\nindex value - " + i + "\n" +
"userlist size - " + usersList.size());
}
}
#Override
public void onClick(View view) {
int buttonTag = Integer.valueOf(String.valueOf(view.getTag()));
TinderCardView topCardOnStack = tinderStackLayout.getTopCardOnStack();
topCardOnStack.handleButtonPressed(buttonTag);
// if (buttonTag == 1) { // TODO - move logic to card class
// userViewModel.delete(usersList.get(0));
// //fetchUserList();
// }
}
private void initViewsAndListeners() {
tinderStackLayout = findViewById(R.id.activity_main_tinder_stack_layout);
mDeleteButton = findViewById(R.id.activity_main_delete_button);
mPassButton = findViewById(R.id.activity_main_pass_button);
mApproveButton = findViewById(R.id.activity_main_approve_button);
mDeleteButton.setOnClickListener(this);
mApproveButton.setOnClickListener(this);
mPassButton.setOnClickListener(this);
}
As you can see I have log messages all over so you can understand what I am about to show you now. I am getting one extra user, "barak obama1" user first and then after that all other 3 -
The livedata figures out that there was 1 user in the list, adds in as a card and than the DB creates new objects and the livedata recalls the method, adding 3 more users.
Why is this happening?? I would glady kiss someone's leg if he solves this issue, no joke.
edit -
here is my ViewModel -
public class UserViewModel extends AndroidViewModel {
private UserRepository repository;
private LiveData<List<User>> allUsers;
public UserViewModel(#NonNull Application application) {
super(application);
repository = new UserRepository(application);
allUsers = repository.getAllUsers();
}
public void insert(User user) {
repository.insert(user);
}
public void update(User user) {
repository.update(user);
}
public void delete(User user) {
repository.delete(user);
}
public void deleteAllUsers(){
repository.deleteAllUsers();
}
public LiveData<List<User>> getAllUsers() {
Log.d("inside observe - ", "inside viewmodel");
return allUsers;
}
}
and my respository -
public class UserRepository {
private UserDao userDao;
private LiveData<List<User>> allUsers;
public UserRepository(Application application) {
UserDatabase database = UserDatabase.getInstance(application);
userDao = database.userDao();
allUsers = userDao.getAllUsers();
}
public void insert(User user) {
new InsertUserAsyncTask(userDao).execute(user);
}
public void update(User user) {
new UpdateUserAsyncTask(userDao).execute(user);
}
public void delete(User user) {
new DeleteUserAsyncTask(userDao).execute(user);
}
public void deleteAllUsers() {
new DeleteAllUsersAsyncTask();
}
public LiveData<List<User>> getAllUsers() {
Log.d("inside observe - ", "inside repository");
return allUsers;
}
//TODO - migrate all 4 async tasks into one.
private static class InsertUserAsyncTask extends AsyncTask<User, Void, Void> {
private UserDao userDao;
private InsertUserAsyncTask(UserDao userDao) {
this.userDao = userDao;
}
#Override
protected Void doInBackground(User... users) {
userDao.insert(users[0]);
return null;
}
}
private static class UpdateUserAsyncTask extends AsyncTask<User, Void, Void> {
private UserDao userDao;
private UpdateUserAsyncTask(UserDao userDao) {
this.userDao = userDao;
}
#Override
protected Void doInBackground(User... users) {
userDao.update(users[0]);
return null;
}
}
private static class DeleteUserAsyncTask extends AsyncTask<User, Void, Void> {
private UserDao userDao;
private DeleteUserAsyncTask(UserDao userDao) {
this.userDao = userDao;
}
#Override
protected Void doInBackground(User... users) {
userDao.delete(users[0]);
return null;
}
}
private static class DeleteAllUsersAsyncTask extends AsyncTask<Void, Void, Void> {
private UserDao userDao;
private DeleteAllUsersAsyncTask() {
this.userDao = userDao;
}
#Override
protected Void doInBackground(Void... voids) {
userDao.deleteAllUsers();
return null;
}
}
}
edit -
here is my dao -
#Dao
public interface UserDao {
#Insert
void insert(User user);
#Update
void update(User user);
#Delete
void delete(User user);
#Query("DELETE FROM user_table")
void deleteAllUsers();
#Query("SELECT * FROM user_table ORDER BY id DESC")
LiveData<List<User>> getAllUsers();
}
Insert all users in 1 transaction.
2 approaches:
1. Create a function in dao that receive list of users.
2. Create a transaction in roomDB (google how. Very simple)
I prefer the first one
try to understand
i have made comments
this is raw code just to give you idea
private ArrayList<User> usersList;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.usersList = new ArrayList(); // initalise the array list here
fetchUserList();
}
private void fetchUserList() {
userViewModel = ViewModelProviders.of(this).get(UserViewModel.class);
final Observer<List<User>> userObserver = users -> {
Log.d("inside observe - ", "inside main activity, list size - " + users.size());
usersList = (ArrayList) users; //dont do this ! instead follow the below instructions
// to do
for(User user : users){
if(!usersList.contains(user)){
usersList.add(user);
}
}
// to do ends here
initViewsAndListeners();
addCards();
};
userViewModel.getAllUsers().observe(this, userObserver);
}
}
look what i have done:
initalised the usersList
and when observing the users live data i use loop and in that loop i
check if this user is already added
do you get it ?
I am using room as data store for my app. I am trying to save a list of sessions from a successful network call in viewmodel class. I have used a repository for interacting with the dao and asynctask for making crud operations async.
Now, I'm trying to display the "saved data" in a recyclerview but it shows nothing. On inspection of my database table, I find that nothing was saved. Here's my model class:
#Entity(tableName = "sessions")
public class Sessions{
// #PrimaryKey(autoGenerate = true)
// public int id;
#SerializedName("prg_session_image")
public String sessionImage;
#SerializedName("prg_session_name")
public String session_name;
#SerializedName("prg_session_id") // used session id as PK
#PrimaryKey
#NonNull
public String prog_sessionId;
#SerializedName("prg_session_description")
public String session_desc;
#SerializedName("reference_id")
public String reference_id;
#SerializedName("prg_name")
public String program_name;
#SerializedName("batch_name")
public String batch_name;
#SerializedName("player_count")
public String participants_count;
#SerializedName("prg_session_focus_points")
public String session_focus_points;
#SerializedName("prg_session_equipment")
public String equipments_reqd;
#SerializedName("session_complete")
public String is_complete;
public Sessions() {
}
// public int getId() {
// return id;
// }
public String getSessionImage() {
return sessionImage;
}
public void setSessionImage(String sessionImage) {
this.sessionImage = sessionImage;
}
public String getSession_name() {
return session_name;
}
public void setSession_name(String session_name) {
this.session_name = session_name;
}
public String getProg_sessionId() {
return prog_sessionId;
}
public void setProg_sessionId(String prog_sessionId) {
this.prog_sessionId = prog_sessionId;
}
public String getSession_desc() {
return session_desc;
}
public void setSession_desc(String session_desc) {
this.session_desc = session_desc;
}
public String getReference_id() {
return reference_id;
}
public void setReference_id(String reference_id) {
this.reference_id = reference_id;
}
public String getProgram_name() {
return program_name;
}
public void setProgram_name(String program_name) {
this.program_name = program_name;
}
public String getBatch_name() {
return batch_name;
}
public void setBatch_name(String batch_name) {
this.batch_name = batch_name;
}
public String getParticipants_count() {
return participants_count;
}
public void setParticipants_count(String participants_count) {
this.participants_count = participants_count;
}
public String getSession_focus_points() {
return session_focus_points;
}
public void setSession_focus_points(String session_focus_points) {
this.session_focus_points = session_focus_points;
}
public String getEquipments_reqd() {
return equipments_reqd;
}
public void setEquipments_reqd(String equipments_reqd) {
this.equipments_reqd = equipments_reqd;
}
public String getIs_complete() {
return is_complete;
}
public void setIs_complete(String is_complete) {
this.is_complete = is_complete;
}
}
And Dao class:
#Dao
public interface SessionsDAO {
// #Insert
// LiveData<List<Sessions>> saveSessions(List<Sessions> sessions);
#Insert
void addSessions(List<Sessions> list);
#Query("select * from sessions")
LiveData<List<Sessions>> getAllSessions();
#Query("select * from sessions where prog_sessionId = :id")
Sessions getSessionById(String id);
}
In repository, I have asynctasks for various operations with the Dao:
public class SessionsRepository {
public SessionsDAO dao;
private MutableLiveData<List<Sessions>> querySingleSession;
private LiveData<List<Sessions>> allSessions;
public SessionsRepository(Application application){
SportsDatabase database = SportsDatabase.getInstance(application);
dao = database.sessionsDAO();
querySingleSession = new MutableLiveData<>();
allSessions = dao.getAllSessions();
}
public void saveSessions(List<Sessions> sessions){
new SaveSessionsTask(dao).execute(sessions);
}
public LiveData<List<Sessions>> getAllSessions() {
return allSessions;
}
public void getSessionById(List<Sessions> sessions){
querySingleSession.setValue(sessions);
}
public class SaveSessionsTask extends AsyncTask<List<Sessions>, Void, Void>{
private SessionsDAO dao;
public SaveSessionsTask(SessionsDAO dao) {
this.dao = dao;
}
#Override
protected Void doInBackground(List<Sessions>... lists) {
dao.addSessions(lists[0]);
return null;
}
}
// public void getSessions(){
// new GetSessionsTask(dao).execute();
// }
// public class GetSessionsTask extends AsyncTask<Void, >
}
I am trying to at the moment save all the results from network call and display them from the database. Here's my operation in viewmodel class:
public class HomeSessionsViewModel extends AndroidViewModel {
private static final String TAG = HomeSessionsViewModel.class.getSimpleName();
private MutableLiveData<SessionDetails> liveDetails;
private SessionsRepository repository;
public HomeSessionsViewModel(#NonNull Application application) {
super(application);
repository = new SessionsRepository(application);
}
// public HomeSessionsViewModel (Application application){
// repository = new SessionsRepository(application);
// }
public MutableLiveData<SessionDetails> getSessions(){
if (liveDetails == null){
liveDetails = new MutableLiveData<>();
fetchSessions();
}
return liveDetails;
}
private void fetchSessions(){
String coachId = "4086";
Call<SessionDetails> call = RestClient.getRestInstance().getSessionsService().fetchSessions(coachId);
call.enqueue(new Callback<SessionDetails>() {
#Override
public void onResponse(Call<SessionDetails> call, Response<SessionDetails> response) {
if (response.isSuccessful()){
SessionDetails details = response.body();
List<Sessions> sessions = details.getSessions();
Log.d(TAG, "N/w sesh size:\t" + sessions.size());
liveDetails.setValue(details); // now just displaying from network
saveSessions(sessions);
}
}
#Override
public void onFailure(Call<SessionDetails> call, Throwable t) {
}
});
}
private void saveSessions(List<Sessions> sessions) {
repository.saveSessions(sessions);
}
public LiveData<List<Sessions>> fetchSessionsDB(){
return repository.getAllSessions();
}
}
and in ui controller (fragment), I have called the viewmodel's fetchSessionsDB() method but no data is shown. The network request works well as I was displaying from there before adding room. What could be wrong here? Thank you.
API Response:
{
"session_details": [
{
"prg_session_name": "Session-16",
"prg_session_id": "987",
"prg_session_equipment": null,
"prg_session_description": "",
"prg_session_focus_points": "",
"prg_session_image": "http://devsports.copycon.in/includes/uploads/Jellyfish5.jpg",
"session_complete": "0",
"prg_name": "cricket coaching",
"reference_id": "293",
"batch_id": "57",
"batch_name": "Batch 3",
"player_count": "10"
}, .... ]}
and SessionDetails POJO:
public class SessionDetails {
#SerializedName("session_details")
#Expose
private List<Sessions> sessions;
#SerializedName("status")
private String status;
#SerializedName("message")
private String msg;
public List<Sessions> getSessions() {
return sessions;
}
}
fragment class where db data should be displayed:
private void populateSessions() {
sessionsRV = fragmentBinding.sessionsRV;
sessionsRV.setHasFixedSize(false);
LinearLayoutManager hlm = new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false);
sessionsRV.setLayoutManager(hlm);
sessionsViewModel = ViewModelProviders.of(this).get(HomeSessionsViewModel.class);
// sessionsViewModel.fetchSessions(""); // TODO: 3/16/2019 Use coach id from db
// calling db from viewmodel
sessionsViewModel.fetchSessionsDB().observe(this, new Observer<List<Sessions>>() {
#Override
public void onChanged(#Nullable List<Sessions> sessions) {
sessionsAdapter = new SessionsAdapter(getActivity(), sessions);
sessionsRV.setAdapter(sessionsAdapter);
Log.d(TAG, "Sessions Count:\t" + sessionsAdapter.getItemCount()); // logs 0
}
});
// previously from network directly displayed
// sessionsViewModel.getSessions().observe(this, new Observer<SessionDetails>() {
// #Override
// public void onChanged(#Nullable SessionDetails details) {
// List<Sessions> list = details.getSessions();
// sessionsAdapter = new SessionsAdapter(getActivity(), list);
// sessionsRV.setAdapter(sessionsAdapter);
// Log.d(TAG, "Sessions Count:\t" + sessionsAdapter.getItemCount());
// }
// });
}
Sports Database class:
#Database(entities = {CoachDB.class, Sessions.class}, version = 1, exportSchema = false)
public abstract class SportsDatabase extends RoomDatabase {
private static SportsDatabase instance;
public abstract CoachDAO coachDAO();
public abstract SessionsDAO sessionsDAO();
public static synchronized SportsDatabase getInstance(Context context) {
if (instance == null){
instance = Room.databaseBuilder(context.getApplicationContext(), SportsDatabase.class, "sports_db")
.fallbackToDestructiveMigration()
.build();
}
return instance;
}
}
I have solved this issue by modifying my #insert method in dao like
#Dao
public interface SessionsDAO {
#Insert
void addSessions(List<Sessions> sessions);
#Query("select * from sessions")
LiveData<List<Sessions>> getAllSessions();
#Query("select * from sessions where prog_sessionId = :id")
Sessions getSessionById(String id);
}
and run my async task with a list of sessions as input and it worked successfully.
private void saveSessions(List<Sessions> sessions) {
new SaveSessionsTask(dao).execute(sessions);
}
public class SaveSessionsTask extends AsyncTask<List<Sessions>, Void, Void> {
private SessionsDAO dao;
public SaveSessionsTask(SessionsDAO dao) {
this.dao = dao;
}
#Override
protected Void doInBackground(List<Sessions>... lists) {
dao.addSessions(lists[0]);
return null;
}
}
I'm learning MVVM with databinding and I have problem with null objects in my TaskViewModel. I've created two constructors, and one of them is empty, but I have null in Task object. When I had only one constructor in viewmodel it made error with instance of viewmodel. What I'm doing wrong?
If you want more code please let me know.
TaskViewModel
public class TaskViewModel extends ViewModel {
private TaskRepository mTaskRepository;
private LiveData<List<Task>> taskMutableLiveData;
private String TAG = "TaskViewModel = ";
private Context context;
public final ObservableField<String> description = new ObservableField<>();
public final ObservableField<String> date = new ObservableField<>();
public final ObservableField<String> time = new ObservableField<>();
public TaskViewModel(TaskRepository taskRepository) {
mTaskRepository = taskRepository;
}
public TaskViewModel() {
}
public void insert() {
addTask(description.get(), date.get(), time.get());
Log.d(TAG, "DATA " + description + " / " + date + " / " + time);
}
public void addTask(String description, String date, String time) {
Task addTask = new Task(description, date, time);
mTaskRepository.insert(addTask);
}
}
TaskRepository
public class TaskRepository {
private TaskDao taskDao;
private LiveData<List<Task>> allTasks;
public TaskRepository(Application application) {
TaskDatabase database = TaskDatabase.getInstance(application);
taskDao = database.taskDao();
allTasks = taskDao.getAllTasks();
}
public void insert(Task task) {
new InsertTaskAsyncTask(taskDao).execute(task);
}
public void update(Task task) {
new UpdateTaskAsyncTask(taskDao).execute(task);
}
public void delete(Task task) {
new DeleteTaskAsyncTask(taskDao).execute(task);
}
public LiveData<List<Task>> getAllTasks() {
return allTasks;
}
private static class InsertTaskAsyncTask extends AsyncTask<Task, Void, Void>
{
private TaskDao taskDao;
private InsertTaskAsyncTask(TaskDao taskDao) {
this.taskDao = taskDao;
}
#Override
protected Void doInBackground(Task... tasks) {
taskDao.insert(tasks[0]);
return null;
}
}
private static class DeleteTaskAsyncTask extends AsyncTask<Task, Void, Void>
{
private TaskDao taskDao;
private DeleteTaskAsyncTask(TaskDao taskDao) {
this.taskDao = taskDao;
}
#Override
protected Void doInBackground(Task... tasks) {
taskDao.delete(tasks[0]);
return null;
}
}
private static class UpdateTaskAsyncTask extends AsyncTask<Task, Void, Void>
{
private TaskDao taskDao;
private UpdateTaskAsyncTask(TaskDao taskDao) {
this.taskDao = taskDao;
}
#Override
protected Void doInBackground(Task... tasks) {
taskDao.update(tasks[0]);
return null;
}
}
}
Instead of making parameterized constructor for ViewModel, make your initialization in default constructor. because after all, we're consuming ViewModel from ViewModelProviders which has default constructor call from it (if you haven't customized it yet).
So do like below,
public class TaskViewModel extends ViewModel {
private TaskRepository mTaskRepository;
...
public TaskViewModel() {
mTaskRepository = new TaskRepository(); // This will avoid your null pointer exception
}
public void insert() {
addTask(description.get(), date.get(), time.get());
Log.d(TAG, "DATA " + description + " / " + date + " / " + time);
}
public void addTask(String description, String date, String time) {
Task addTask = new Task(description, date, time);
mTaskRepository.insert(addTask);
}
}
If you want to have Context in your ViewModel, use AndroidViewModel instead which will provide you application context in default constructor implementation. but don't store it in your ViewModel though.
When reading data from DB using ROOM and returning LiveData, getValue() method returns null. I have been unable to understand what is going wrong for a while now. Can you please assist with this issue? There is data in the database, it seems like it is more of an issue of how I am using LiveData objects.
Activity:
public class ExercisesViewActivity extends AppCompatActivity implements View.OnClickListener {
private ExerciseViewModel exerciseViewModel;
private ExercisesAdapter recyclerViewerAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_exercises_view);
Toolbar toolbar = findViewById(R.id.toolbar_exercise_view_activity);
toolbar.setTitle("Exercises");
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setDisplayShowHomeEnabled(true);
}
RecyclerView recyclerView = findViewById(R.id.exercise_view_recycle_viewer);
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getApplicationContext());
recyclerView.setLayoutManager(layoutManager);
this.recyclerViewerAdapter = new ExercisesAdapter();
recyclerView.setAdapter(recyclerViewerAdapter);
this.exerciseViewModel = ViewModelProviders.of(this).get(ExerciseViewModel.class);
this.exerciseViewModel.setFilters("", "");
// this.exerciseViewModel.selectAll();
this.exerciseViewModel.select().observe(this, exercises -> {
if (exercises != null) {
this.recyclerViewerAdapter.updateDataset(exercises);
}
});
Button button = findViewById(R.id.test_button);
button.setOnClickListener(this);
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
break;
}
return true;
}
#Override
public void onClick(View v) {
this.exerciseViewModel.setFilters("", "");
// this.exerciseViewModel.select().observe(this, exercises -> {
// if (exercises != null) {
// this.recyclerViewerAdapter.updateDataset(exercises);
// }
// });
}
}
ViewModel:
public class ExerciseViewModel extends AndroidViewModel {
ExercisesRepository repository;
MutableLiveData<List<Exercise>> data;
public ExerciseViewModel(Application application) {
super(application);
this.data = new MutableLiveData<>();
this.repository = new ExercisesRepository(application);
}
public void setFilters(String muscleGroups, String type) {
LiveData<List<Exercise>> listLiveData = this.repository.filterSelect(muscleGroups, type);
this.data.setValue(listLiveData.getValue());
}
public void selectAll() {
// this.data.setValue(this.repository.selectAll().getValue());
}
public LiveData<List<Exercise>> select() {
return data;
}
public void insert(Exercise exercise) {
this.repository.insert(exercise);
}
}
Repository:
public class ExercisesRepository {
private ExerciseDao dao;
public ExercisesRepository(Application context) {
WorkoutRoomDatabase database = WorkoutRoomDatabase.getDb(context);
this.dao = database.exerciseDao();
}
public LiveData<List<Exercise>> filterSelect(String muscleGroups, String type) {
return this.dao.filterSelect("%" + muscleGroups + "%", "%" + type + "%");
}
public LiveData<List<Exercise>> selectAll() {
return this.dao.selectAll();
}
public void insert(Exercise exercise) {
new insertAsyncTask(this.dao).execute(exercise);
}
private static class insertAsyncTask extends AsyncTask<Exercise, Void, Void> {
private ExerciseDao exerciseDao;
insertAsyncTask(ExerciseDao dao) {
exerciseDao = dao;
}
#Override
protected Void doInBackground(final Exercise... params) {
exerciseDao.insert(params[0]);
return null;
}
}
}
DAO:
#Dao
public interface ExerciseDao {
#Query("SELECT * FROM exercises WHERE muscleGroups LIKE :muscleGroup AND type LIKE :type")
LiveData<List<Exercise>> filterSelect(String muscleGroup, String type);
#Query("SELECT * FROM exercises")
LiveData<List<Exercise>> selectAll();
#Insert
void insert(Exercise exercise);
#Update
void update(Exercise exercise);
#Delete
void delete(Exercise exercise);
#Query("DELETE FROM exercises")
void deleteAll();
}
UPDATE:
If in my viewModel I change filter function to return a new LiveData object, correct data is being fetched:
public LiveData<List<Exercise>> filterSelect(String muscleGroups, String type) {
return this.dao.filterSelect("%" + muscleGroups + "%", "%" + type + "%");
}
But then in my Activity I need to create a new observer, as now data is provided by new instances of LiveData:
#Override
public void onClick(View v) {
this.exerciseViewModel.setFilters("Biceps", "weight");
this.exerciseViewModel.select().observe(this, exercises -> {
if (exercises != null) {
this.recyclerViewerAdapter.updateDataset(exercises);
}
});
}
This is definetely now the right way of doing this :/
Rather than MutableLiveData you should look at MediatorLiveData which will allow you to propopgate the changes from the repository.
This would look something like below where you add the repository data as a data source to the mediatorLiveData. This will then trigger a callback when you receive data from the repository, which you can then emit from the mediatorLiveData by calling setValue.
public class ExerciseViewModel extends AndroidViewModel {
ExercisesRepository repository;
MediatorLiveData<List<Exercise>> mediatorLiveData = new MediatorLiveData<>();
public ExerciseViewModel(Application application) {
super(application);
this.repository = new ExercisesRepository(application);
}
public void setFilters(String muscleGroups, String type) {
LiveData<List<Exercise>> repositoryLiveData = this.repository.filterSelect(muscleGroups, type);
mediatorLiveData.addSource(repositoryLiveData, exercisList -> {
mediatorLiveData.removeSource(repositoryLiveData);
mediatorLiveData.setValue(exercisList);
});
}
public LiveData<List<Exercise>> select() {
return mediatorLiveData;
}
You trying to fetch like search in filterSelete with empty value. this.exerciseViewModel.setFilters("", "");.
This filter is basically a issue. You can't do empty like search.
Instated of this, you can use loadAll query.
#Query("SELECT * FROM exercises")
LiveData<List<Exercise>> loadAll();
Edit:
public LiveData<List<Exercise>> filterSelect(String muscleGroups, String type) {
if(muscleGroups == "" && type == "") {
return this.dao.loadAll()
}else {
return this.dao.filterSelect("%" + muscleGroups + "%", "%" + type + "%");
}
Hope, It's will solve your problem.
I managed to solve this issue by using MediatorLiveData and Transformations.
So now my ViewModel constructor looks like this:
public ExerciseViewModel(Application application) {
super(application);
this.repository = new ExercisesRepository(application);
this.filterMutableLiveData = new MutableLiveData<>();
ExercisesFilter filter = new ExercisesFilter();
filter.muscleGroup = "";
filter.type = "";
this.filterMutableLiveData.setValue(filter);
LiveData<List<Exercise>> source = Transformations.switchMap(
this.filterMutableLiveData, value -> repository.filterSelect(value.muscleGroup, value.type)
);
this.data.addSource(source, val -> this.data.setValue(val));
}
And I have added this method:
public void setFilterMutableLiveData(String muscleGroups, String type) {
ExercisesFilter filter1 = new ExercisesFilter();
filter1.muscleGroup = muscleGroups;
filter1.type = type;
this.filterMutableLiveData.setValue(filter1);
}
With this approach, every time filters are updated, the function passed to Transformations.switchMap is triggered to fetch new data. In the end, new LiveData object is added to MediatorLiveData object, therefore one observer can be utilised for all sources.
Source: https://github.com/googlesamples/android-sunflower/blob/99df6189af927b5cc477167923bbbd5a4ca1ece9/app/src/main/java/com/google/samples/apps/sunflower/viewmodels/PlantListViewModel.kt#L42
My app is using Android's Architecture components library and is displaying a list of items fetched from a paginated REST api with an infinite scroll effect.
What I'm trying to do is to use the Paging Library in conjunction with a NetworkBoundResource, so that when the user scrolls down the list, the next items are fetched from the database and displayed if they exist, and the API is simultaneously called to update items in DB.
I could not find any example of these two patterns cohabiting.
Here is the DAO:
#Query("SELECT * FROM items ORDER BY id DESC")
LivePagedListProvider<Integer,MyItem> loadListPaginated();
Here is my NetworkBoundResource implementation:
public class PagedListNetworkBoundResource extends NetworkBoundResource<PagedList<MyItem>, List<MyItem>> {
#Override
protected void saveCallResult(#NonNull List<MyItem> items) {
// Inserting new items into DB
dao.insertAll(items);
}
#Override
protected boolean shouldFetch(#Nullable PagedList<MyItem> data) {
return true;
}
#NonNull
#Override
protected LiveData<PagedList<MyItem>> loadFromDb() {
return Transformations.switchMap(dao.loadListPaginated().create(INITIAL_LOAD_KEY, PAGE_SIZE),
new Function<PagedList<MyItem>, LiveData<List<MyItem>>>() {
#Override
public LiveData<PagedList<MyItem>> apply(final PagedList<MyItem> input) {
// Here I must load nested objects, attach them,
// and return the fully loaded items
}
});
}
#NonNull
#Override
protected LiveData<ApiResponse<List<MyItem>>> createCall() {
// I don't get the current paged list offset to perform a call to the API
return ...;
}
}
I also search lot about NetworkBoundResource i came to conclusion that NetworkBoundResource & Paging Lib its not related to each other. They both have there own functionality
As per article give by google about paging library
https://developer.android.com/topic/libraries/architecture/paging.html
1.for loading data from local db you need use DataSource
My Dao
#Dao
public interface UserDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(User... user);
#Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(List<User> users);
#Query("Select * from User ")
public abstract DataSource.Factory<Integer,User> getList();
}
2.then requesting data from network we need implement BoundaryCallback class with LivePagedListBuilder
public class UserBoundaryCallback extends PagedList.BoundaryCallback<User> {
public static final String TAG = "ItemKeyedUserDataSource";
GitHubService gitHubService;
AppExecutors executors;
private MutableLiveData networkState;
private MutableLiveData initialLoading;
public UserBoundaryCallback(AppExecutors executors) {
super();
gitHubService = GitHubApi.createGitHubService();
this.executors = executors;
networkState = new MutableLiveData();
initialLoading = new MutableLiveData();
}
public MutableLiveData getNetworkState() {
return networkState;
}
public MutableLiveData getInitialLoading() {
return initialLoading;
}
#Override
public void onZeroItemsLoaded() {
//super.onZeroItemsLoaded();
fetchFromNetwork(null);
}
#Override
public void onItemAtFrontLoaded(#NonNull User itemAtFront) {
//super.onItemAtFrontLoaded(itemAtFront);
}
#Override
public void onItemAtEndLoaded(#NonNull User itemAtEnd) {
// super.onItemAtEndLoaded(itemAtEnd);
fetchFromNetwork(itemAtEnd);
}
public void fetchFromNetwork(User user) {
if(user==null) {
user = new User();
user.userId = 1;
}
networkState.postValue(NetworkState.LOADING);
gitHubService.getUser(user.userId,20).enqueue(new Callback<List<User>>() {
#Override
public void onResponse(Call<List<User>> call, Response<List<User>> response) {
executors.diskIO().execute(()->{
if(response.body()!=null)
userDao.insert(response.body());
networkState.postValue(NetworkState.LOADED);
});
}
#Override
public void onFailure(Call<List<User>> call, Throwable t) {
String errorMessage;
errorMessage = t.getMessage();
if (t == null) {
errorMessage = "unknown error";
}
Log.d(TAG,errorMessage);
networkState.postValue(new NetworkState(Status.FAILED, errorMessage));
}
});
}
}
3.My VM Code to load data from DB + Network
public class UserViewModel extends ViewModel {
public LiveData<PagedList<User>> userList;
public LiveData<NetworkState> networkState;
AppExecutors executor;
UserBoundaryCallback userBoundaryCallback;
public UserViewModel() {
executor = new AppExecutors();
}
public void init(UserDao userDao)
{
PagedList.Config pagedListConfig =
(new PagedList.Config.Builder()).setEnablePlaceholders(true)
.setPrefetchDistance(10)
.setPageSize(20).build();
userBoundaryCallback = new UserBoundaryCallback(executor);
networkState = userBoundaryCallback.getNetworkState();
userList = (new LivePagedListBuilder(userDao.getList(), pagedListConfig).setBoundaryCallback(userBoundaryCallback))
.build();
}
}
This assumes that each item in the callback has contains an index/offset. Typically that is not the case - the items may only contain ids.