I'm trying to re-structure my codebase to implement MVVM architecture and 3-layered architecture using Android Architecture Components.
The MainActivity.java file contains a Fragment, and this Fragment is where most of the user interactions take place. The user provides an input, and based on that input I want to be able to hit the Firebase Firestore database to fetch the relevant data and then display it on the UI. This approach should follow the Separation of Concerns principle.
Currently, upon receiving user input on the Fragment, I relay the input to its parent activity, i.e., MainActivity.java using an Interface, and then call a function FetchUserRequestedData. Once I receive the data, I display it appropriately on the UI (not a part of the Fragment).
However, I want my 1) business logic layer, 2) database access layer, and 3) UI layer in separate packages. (To ensure that I follow the n-layered architecture pattern)
Also, with Firestore as the database for my app, I'm not sure how the flow of both, input from user and output data from Firestore should be handled all the way from the data layer to the UI model. Note that Firestore database calls are asynchronous.
How can I set up my codebase to follow the correct communication model between the three layers so that it follows both MVVM and n-layered architecture?
Code is like this:
MainActivity.java
public class MainActivity extends AppCompatActivity implements FragmentClass.DataPasser {
FirebaseAuth firebaseAuth;
FirebaseUser firebaseUser;
private ArrayList<DataType> dataList = new ArrayList<>();
DataProvider dataProvider = new DataProvider();
.
.
.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
.
.
.
}
#Override
protected void onStart() {
super.onStart();
.
.
.
}
#Override
public void onUserInput(DataType input) {
dataList = dataProvider.ProvideData(input);
//update UI using this dataList
}
}
FragmentClass.java
public class FragmentClass extends CustomFragmentClass {
DataPasser dataPasser;
#Override
public void onAttach(Context context) {
super.onAttach(context);
dataPasser = (DataPasser) context;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle inState) {
super.onCreateView(inflater, container, inState);
// Do app specific setup logic.
return Content.getContentView();
}
#Override
public void userProvidedInput(DataType userInput) { //part of CustomFragmentClass
super.userProvidedInput(userInput);
dataPasser.onUserInput(userInput); //defined in MainActivity.java
}
public interface DataPasser { //interface class to send user input to main activity
public void onUserInput(DataType input);
}
}
DataProvider.java
public class DataProvider {
private ArrayList<DataType> dataList = new ArrayList<>();
DataFetcher dataFetcher = new DataFetcher();
public ArrayList<DataType> ProvideData(String userInput) {
dataList = dataFetcher(userInput);
return dataList;
}
private ArrayList<DataType> dataFetcher(String userInput) {
ArrayList<DataType> fetchedData = new ArrayList<>();
fetchedData = dataFetcher.DatabaseCaller(userInput);
return fetchedData;
}
}
DataFetcher.java
public class DataFetcher {
ArrayList<DataType> dataList;
FirebaseFirestore firestore = FirebaseFirestore.getInstance();
public ArrayList<DataType> DatabaseCaller(String userInput) {
//formulate query based on userInput
//hit database and fetch dataList
return dataList;
}
}
Related
I'm new with the ViewModel and I understand that it's a powerful and easy way to communicate with fragments.
My problem is the following : How to load the data retrieved in the SplashActivity in the ViewModel of the mainActivity ?
My app achitecture is the following :
SplashActivity : retrieve data with retrofit and store it into a List
Main Activity : contains two fragments displaying the data in different ways
Here is a piece of code showing my implementation.
SplashActivity
public class SplashActivity extends AppCompatActivity {
private final String TAG = "TAG.SplashActivity";
public static List<Toilet> toiletList = new ArrayList<>(); // HERE IS THE DATA I WANT TO
RETRIEVE IN THE MAIN ACTIVITY
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/*Create handle for the RetrofitInstance interface*/
GetDataService service = ...;
// MY STUFF RETROFIT including
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
intent.putExtra("toiletList", (Serializable) toiletList);
startActivity(intent);
finish();
}
}
MainActivity
public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {
private final String TAG = getClass().getName();
private List<Toilet> toiletList = new ArrayList<>();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent= getIntent();
Serializable s = intent.getSerializableExtra("toiletList");
// Check type and cast
if (s instanceof List<?>) {
for (Object o : (List<?>) s) {
if (o instanceof Toilet) {
toiletList.add((Toilet) o);
}
}
}
// SETTING UP FRAGMENTS
}
}
FragmentExample
public class MainFragment extends Fragment {
public static List<Toilet> toiletArrayList = new ArrayList<>();
private final String TAG = this.getClass().getName();
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_main, container, false);
// SETTING UP UI
return view;
}
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
ToiletListViewModel model = ViewModelProviders.of(this).get(ToiletListViewModel.class);
model.getToiletList().observe(this, new Observer<List<Toilet>>() {
#Override
public void onChanged(#Nullable List<Toilet> toilets) {
// update UI
}
});
}
}
ToiletListViewModel
public class ToiletListViewModel extends ViewModel {
private final String TAG = getClass().getName();
private MutableLiveData<List<Toilet>> toiletList;
public LiveData<List<Toilet>> getToiletList() {
if (toiletList == null) {
toiletList = new MutableLiveData<>();
loadToilets();
}
return toiletList;
}
private void loadToilets() {
// asynchronously fetch toilets
// HERE IS MY PROBLEM : How to access the toiletList retrieved
in the SplashActivity ?
toiletList.setValue(SplashActivity.toiletList);
}
#Override
protected void onCleared() {
super.onCleared();
Log.d(TAG, "onCleared() called");
}
}
I hope that's clear. If you want any further info, fell free to ask !
Best
You can share your ToiletListViewModel between the MainActivity and its Fragments.
So what you need is to provide your ViewModel with MainActivity scope (It means you bound the lifecycle of your ViewModel to your Activity) and call initToilets then child fragments can easily retrieve this ViewModel and observe on its LiveData.
ToiletListViewModel:
public class ToiletListViewModel extends ViewModel {
private MutableLiveData<List<Toilet>> toiletList = new MutableLiveData();
public LiveData<List<Toilet>> getToiletList() {
return toiletList;
}
private void initToilets(List<Toilet> toilets) {
toiletList.setValue(toilets);
}
}
MainActivity:
public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {
private final String TAG = getClass().getName();
private List<Toilet> toiletList = new ArrayList<>();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent= getIntent();
Serializable s = intent.getSerializableExtra("toiletList");
// Check type and cast
if (s instanceof List<?>) {
for (Object o : (List<?>) s) {
if (o instanceof Toilet) {
toiletList.add((Toilet) o);
}
}
}
ToiletListViewModel vm = ViewModelProviders.of(this).get(ToiletListViewModel.class);
vm.initToilets(toiletList);
// SETTING UP FRAGMENTS
}
}
So, when setValue is called, Fragments that listen to the toiletList live data will be notified.
Note:
You can create a shared ViewModel without providing it on MainActivity, instead of calling
ViewModelProviders.of(this).get(ToiletListViewModel.class);
in your Fragment do
ViewModelProviders.of(getActivity()).get(ToiletListViewModel.class);
In order to get use out of the a view model, you need to store a reference to it's instance in your activities and then interface with them to modify data.
I would first of all suggest to you that you read the developer guide on View Model.
When you are set-up and storing a reference to the model in your activities and fragments, you could add a method to the model, like setToilets(List<Toilet>), which updates the toilets in the View Model, calls loadToilets() or stores the raw toilets so loadToilets() can later access it and now what toilets to load.
Then you can access all the data that you want to expose from other classes by writing the respective methods, just like you did with the getToiletList(LiveData<Toilet>) -method.
There are two suggestions:
You can add data to list directly (Off Topic):
if (s instanceof List<?>) {
for (Object o : (List<?>) s) {
if (o instanceof Toilet) {
toiletList.add((Toilet) o);
}
}
}
use this instead of:
if (s instanceof List<?>) {
toiletList.addAll((List<Toilet>)s);
}
Back to main topic:
You can take ViewModel instance of Activity instead of this in Fragment. How?
Take ViewModel in activity as below,
ToiletListViewModel model = ViewModelProviders.of(this).get(ToiletListViewModel.class);
& for Fragment share it like this,
ToiletListViewModel model = ViewModelProviders.of(getActivity()).get(ToiletListViewModel.class);
This will share your ViewModel between fragments inside of activity & observe your livedata.
So according to android developers: "Architecture Components provides ViewModel helper class for the UI controller that is responsible for preparing data for the UI. ViewModel objects are automatically retained during configuration changes so that data they hold is immediately available to the next activity or fragment instance."
In the code below there is an asynchronous class that gets called in deleteItem function. My question is this: Does ViewModel also handles the asynchronous calls made inside it or will cause memory leaks?
Thank you
public class BorrowedListViewModel extends AndroidViewModel {
private final LiveData<List<BorrowModel>> itemAndPersonList;
private AppDatabase appDatabase;
public BorrowedListViewModel(Application application) {
super(application);
appDatabase = AppDatabase.getDatabase(this.getApplication());
itemAndPersonList = appDatabase.itemAndPersonModel().getAllBorrowedItems();
}
public LiveData<List<BorrowModel>> getItemAndPersonList() {
return itemAndPersonList;
}
public void deleteItem(BorrowModel borrowModel) {
new deleteAsyncTask(appDatabase).execute(borrowModel);
}
private static class deleteAsyncTask extends AsyncTask<BorrowModel, Void, Void> {
private AppDatabase db;
deleteAsyncTask(AppDatabase appDatabase) {
db = appDatabase;
}
#Override
protected Void doInBackground(final BorrowModel... params) {
db.itemAndPersonModel().deleteBorrow(params[0]);
return null;
}
}
}
I would provide an example, probably you need to modify the code.
First you need a live data change and subscribe to that in your view. Then in the controller you post the value telling the subscriber that something appends. This way asynchronously the view would get alerted.
private MutableLiveData<String> databaseLiveData = new MutableLiveData<>();
...
And in the deleteAsyncTask class you can add:
protected void onPostExecute(Void result) {
databaseLiveData.postValue("some data deleted");
}
And in the BorrowedListViewModel class this method to access from the view add this method:
public LiveData<String> getChanger() {
return databaseLiveData;
}
In the view e.g.Activity add this:
private BorrowedListViewModel mBorrowedListViewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//...
BorrowedListViewModel = ViewModelProviders.of(this).get(BorrowedListViewModel.class);
subscribe();
}
private void subscribe() {
final Observer<String> liveDataChange = new Observer<String>() {
#Override
public void onChanged(#Nullable final String message) {
Log.d("Activity", message);
}
};
liveDataChange.getChanger().observe(this, liveDataChange);
}
Hope this help.
I have a customer detail page which load's a customer detail from "customer" table and allow some editing to some details, one which is customer's location. The customer's location is a spinner which drop down item is loaded from "location" table.
So my current implementation is I have a CustomerActivity which have a CustomerViewModel and LocationViewModel
public class CustomerActivity extends AppCompatActivity {
...
onCreate(#Nullable Bundle savedInstanceState) {
...
customerViewModel.getCustomer(customerId).observe(this, new Observer<Customer>() {
#Override
public void onChanged(#Nullable Customer customer) {
// bind to view via databinding
}
});
locationViewModel.getLocations().observe(this, new Observer<Location>() {
#Override
public void onChanged(#Nullable List<Location> locations) {
locationSpinnerAdapter.setLocations(locations);
}
});
}
}
My question is how do I set the location spinner with the value from "customer" table since both the onChanged from both viewmodels executed order may differ (sometimes customer loads faster while other locations load faster).
I'd considered loading the locations only after the customer is loaded, but is there anyway I can load both concurrently while populating the customer's location spinner with the value from "customer" table?
Yes, you can by using flags.
public class CustomerActivity extends AppCompatActivity {
private Customer customer = null;
private List<Location> locations = null;
...
onCreate(#Nullable Bundle savedInstanceState) {
...
customerViewModel.getCustomer(customerId).observe(this, new Observer<Customer>() {
#Override
public void onChanged(#Nullable Customer customer) {
// bind to view via databinding
this.customer = customer;
if(locations != null){
both are loaded
}
}
});
locationViewModel.getLocations().observe(this, new Observer<Location>() {
#Override
public void onChanged(#Nullable List<Location> locations) {
locationSpinnerAdapter.setLocations(locations);
this.locations = locations;
if(customer != null){
//customer and locations loaded
}
}
});
}
}
It sounds like you want something similar to RxJava's combineLatest operator.
You can implement your own version using MediatorLiveData.
I discover the new android architecture component and I want to test the couple ViewModel / LiveData through a small test application. The latter has two fragments (in a ViewPager), the first creates/updates a list of cards (via an EditText) and the second displays all the cards.
My ViewModel:
public class CardsScanListViewModel extends AndroidViewModel {
private MutableLiveData> cardsLiveData = new MutableLiveData();
private HashMap cardsMap = new HashMap();
public CardsScanListViewModel(#NonNull Application application) {
super(application);
}
public MutableLiveData> getCardsLiveData() {
return this.cardsLiveData;
}
public void saveOrUpdateCard(String id) {
if(!cardsMap.containsKey(id)) {
cardsMap.put(id, new Card(id, new AtomicInteger(0)));
}
cardsMap.get(id).getCount().incrementAndGet();
this.cardsLiveData.postValue(cardsMap);
}
}
My second fragment:
public class CardsListFragment extends Fragment {
CardsAdapter cardsAdapter;
RecyclerView recyclerCardsList;
public CardsListFragment() {}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final CardsScanListViewModel viewModel =
ViewModelProviders.of(this).get(CardsScanListViewModel.class);
observeViewModel(viewModel);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_cards_list, container, false);
recyclerCardsList = v.findViewById(R.id.recyclerCardsList);
recyclerCardsList.setLayoutManager(new LinearLayoutManager(getActivity()));
cardsAdapter = new CardsAdapter(getActivity());
recyclerCardsList.setAdapter(cardsAdapter);
return v;
}
private void observeViewModel(CardsScanListViewModel viewModel) {
viewModel.getCardsLiveData().observe(this, new Observer > () {
#Override
public void onChanged(#Nullable HashMap cards) {
if (cards != null) {
cardsAdapter.setCardsList(cards.values());
}
}
});
}
}
TheHashMap, like my MutableLiveData, update well but my second fragment doesn't receive the information via observer.
You are observing the new instance of ViewModel instead of observing the same ViewModel used by your First Fragment.
final CardsScanListViewModel viewModel =
ViewModelProviders.of(this).get(CardsScanListViewModel.class);
Above code initialize new instance of CardsScanListViewModel for your second fragment CardsListFragment, because you passed this as context.
If you update any data from this fragment it will update in this instance of ViewModel.
It works in your first Fragment because it updates data and observes data from same instance of ViewModel
To keep data common among ViewModels initiate view model by passing activity context instead of fragment context in both the fragments.
final CardsScanListViewModel viewModel =
ViewModelProviders.of(getActivity()).get(CardsScanListViewModel.class);
This will create single instance of CardsScanListViewModel and data will be shared between fragments as they are observing LiveData from single instance of ViewModel.
For confirmation, you need to add notifyDataSetChanged() after updating the list if you haven't done that in adapter itself
private void observeViewModel(CardsScanListViewModel viewModel) {
viewModel.getCardsLiveData().observe(this, new Observer > () {
#Override
public void onChanged(#Nullable HashMap cards) {
if (cards != null) {
cardsAdapter.setCardsList(cards.values());
cardsAdapter.notifyDataSetChanged();
}
}
});
}
I'm currently working on an Android App and i choosed the MVP-Arhitecture.
My Problem is right now, that i need to read and write something from the Database in the Model, but therefor you need a reference to the Context and that is in the View. I want to know, how to get the Context from the View to the Model without breaking the MVP-Architecture (if it is possible).
Thx!!!
Something has to create the model and the presenter i.e.:
new MyModel();
new Presenter();
Usually this is the Activity
#Override
public void onCreate(Bundle savedState) {
Model model = new MyModel();
Presenter presenter = new Presenter(model, this); // this being the View
}
If you are using a database inside of your model you want to use a dependency to do this, maybe called DatabaseReader
#Override
public void onCreate(Bundle savedState) {
DatabaseReader db = new DatabaseReader(this); // this being context
Model model = new MyModel(db);
Presenter presenter = new Presenter(model, this); // this being the View
}
Now you have a class called DatabaseReader that has a Context passed to it through the constructor so you can do "database things", and this class itself is used by the model.
public class DatabaseReader {
private final Context context;
public DatabaseReader(Context context) {
this.context = context;
}
}
and
public class MyModel implements Model {
private final DatabaseReader db;
public MyModel(DatabaseReader db) {
this.db = db;
}
}