My app relies on downloading data from the web. The data is downloaded and stored in Room, but it can change anytime, so the data must be downloaded on each app start (if the internet connection is available). Additionally the data is presented in an adapter, so in a different place than downloading (MainActivity). The problem is that the app freezes for the downloading, storing and reading process, even if the data didn't change.
I have created the following classes:
Dao:
@Dao
public interface AppDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertHolidayDay(HolidayDay day);
@Query("DELETE FROM holiday_day")
void deleteAllHolidayDays();
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertHoliday(Holiday holiday);
@Query("DELETE FROM holiday")
void deleteAllHolidays();
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertFloatingHoliday(FloatingHoliday floatingHoliday);
@Query("DELETE FROM floating_holiday")
void deleteAllFloatingHolidays();
}
Database:
@Database(entities = {Holiday.class, FloatingHoliday.class, HolidayDay.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract AppDao appDao();
}
Repository:
public class AppRepository {
private final AppDao holidayDao;
private final MediatorLiveData<List<HolidayDay>> mergedHolidays = new MediatorLiveData<>();
private List<HolidayDay> fixedHolidaysCache;
private List<FloatingHoliday> floatingHolidaysCache;
public AppRepository(final Application application) {
final AppDatabase database = Room.databaseBuilder(application, AppDatabase.class, "uhc")
.enableMultiInstanceInvalidation()
.allowMainThreadQueries()
.build();
holidayDao = database.appDao();
loadHolidays();
}
@Transaction
public void updateCalendarData(@NonNull final UnusualCalendar calendar) {
holidayDao.deleteAllHolidays();
holidayDao.deleteAllHolidayDays();
holidayDao.deleteAllFloatingHolidays();
calendar.getFixed()
.stream()
.map(HolidayDay::getHolidays)
.flatMap(Collection::stream)
.forEach(holidayDao::insertHoliday);
calendar.getFixed().forEach(holidayDao::insertHolidayDay);
calendar.getFloating().forEach(holidayDao::insertFloatingHoliday);
}
private void loadHolidays() {
final LiveData<List<HolidayDay>> fixedHolidays = holidayDao.getAllHolidayDays();
final LiveData<List<FloatingHoliday>> floatingHolidays = holidayDao.getAllFloatingHolidays();
mergedHolidays.addSource(fixedHolidays, fixed -> {
fixedHolidaysCache = fixed;
mergeHolidays();
});
mergedHolidays.addSource(floatingHolidays, floating -> {
floatingHolidaysCache = floating;
mergeHolidays();
});
}
private void mergeHolidays() {
// magic here
}
}
SharedViewModel:
public class SharedViewModel extends ViewModel {
public static final ViewModelInitializer<SharedViewModel> INITIALIZER = new ViewModelInitializer<>(SharedViewModel.class, creationExtras -> {
final UHCApplication uhcApplication = (UHCApplication) creationExtras.get(ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY);
assert uhcApplication != null;
return new SharedViewModel(uhcApplication.getAppRepository());
});
private final AppRepository repository;
public SharedViewModel(final AppRepository repository) {
this.repository = repository;
}
public void updateData(final UnusualCalendar calendar) {
CompletableFuture.runAsync(() -> repository.updateCalendarData(calendar));
}
}
Downloading in MainActivity:
public class MainActivity extends AppCompatActivity {
private AlertDialog alertDialog;
private final List<HolidayDay> holidayDays = new ArrayList<>();
private MutableLiveData<Boolean> internet;
private volatile boolean downloading;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
configureObservers();
if (Boolean.TRUE.equals(internet.getValue()) && !downloading) {
initiateDownload();
}
// Observe the holiday data
sharedViewModel.getAllHolidayDays().observe(this, days -> {
if (!days.isEmpty()) {
holidayDays.addAll(days);
} else if (Boolean.FALSE.equals(internet.getValue())) {
showNoInternetAlert();
}
});
}
private void initiateDownload() {
downloading = true;
CompletableFuture.supplyAsync(new Downloader()) // just a HttpsUrlConnection + gson
.thenAcceptAsync(data -> {
sharedViewModel.updateData(data);
downloading = false;
});
}
private void configureObservers() {
internet = new MutableLiveData<>(Util.isNetworkAvailable(this));
internet.observe(this, isConnected -> {
if (Boolean.TRUE.equals(isConnected) && alertDialog != null && alertDialog.isShowing() && !downloading) {
CompletableFuture.supplyAsync(new Downloader.UnusualCalendarDownloader())
.thenAccept(sharedViewModel::updateData);
alertDialog.dismiss();
}
});
final ConnectivityManager connectivityManager =
(ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
Util.NETWORK_CAPABILITIES.stream()
.map(new NetworkRequest.Builder()::addTransportType)
.map(NetworkRequest.Builder::build)
.forEach(x -> connectivityManager.registerNetworkCallback(x, new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(@NonNull final Network network) {
super.onAvailable(network);
internet.postValue(true);
}
@Override
public void onLost(@NonNull final Network network) {
super.onLost(network);
internet.postValue(false);
}
}));
}
private void showNoInternetAlert() {
final AlertDialog.Builder alert = new AlertDialog.Builder(this);
alert.setTitle(R.string.no_internet_connection);
alert.setCancelable(false);
alert.setMessage(R.string.no_internet);
alertDialog = alert.show();
}
}
The CompletableFuture is imported from java9.util.concurrent. I can't rely on OnConflictStrategy.REPLACE as entries can sometimes disappear and I want to get rid of them from the database as well.
My preferable solution is to make the app responsive all the time and allow the data to be updated dynamically as they are stored and read (users can see them updating).
I thought about updating the list in the SharedViewModel, but it didn't resolve the freezing issue.
It seems to me that something is simply blocking the main thread, most likely, it is a database operation. By default, Android won't let you do this, but if you make a request in a separate thread, for example, but tell the main thread to wait for it to complete, it can block the main thread. I would suggest turning on the debugger and looking for the last place where the program worked without freezing.
And your constructor looks very strange.