MVP and RxJava - Handling Orientation Changes on Android - android

Im using MVP and RxJava similar to google-samples repo.
And I would like to ask how to correctly handle screen orientation change.

There is another strategy that enables saving presenter state and also Observable's state: retain Fragment. This way you omit standard Android way of saving data into Bundle (which enables only to save simple variables and not the state of network requests.)
Activity:
public class MainActivity extends AppCompatActivity implements MainActivityView {
private static final String TAG_RETAIN_FRAGMENT = "retain_fragment";
MainActivityPresenter mPresenter;
private MainActivityRetainFragment mRetainFragment;
#Override
protected void onCreate(Bundle savedInstanceState) {
initRetainFragment();
initPresenter();
}
private void initRetainFragment() {
FragmentManager fm = getSupportFragmentManager();
mRetainFragment = (MainActivityRetainFragment) fm.findFragmentByTag(TAG_RETAIN_FRAGMENT);
if (mRetainFragment == null) {
mRetainFragment = new MainActivityRetainFragment();
fm.beginTransaction().add(mRetainFragment, TAG_RETAIN_FRAGMENT).commit();
}
}
private void initPresenter() {
mPresenter = mRetainFragment.getPresenter();
mRetainFragment.retainPresenter(null);
if (mPresenter == null) {
mPresenter = new MainActivityPresenter();
}
mPresenter.attachView(this);
}
#Override
protected void onDestroy() {
super.onDestroy();
if (!isFinishing()) {
mRetainFragment.retainPresenter(mPresenter);
return;
}
mPresenter.detachView();
mPresenter = null;
}
}
Retain Fragment:
public class MainActivityRetainFragment extends Fragment {
private MainActivityPresenter presenter;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
public void retainPresenter(MainActivityPresenter presenter) {
this.presenter = presenter;
}
public MainActivityPresenter getPresenter() {
return presenter;
}
}
Notice the way activity lifecycle events are handled. When the Activity is created, retain Fragment is added to the backstack and on lifecycle events it is restored from backstack. retain Fragment does not have any view, it is just a holder for presenter during configuration changes. Notice the main invocation that enables restoring exactly the same fragment (and it's content) from backstack:
setRetainInstance(true)
If you are concerned about memory leaks: every time the presenter is restored presenter's view is restored:
mPresenter.attachView(this);
So the previous Activity reference is replaced by new one.
More about such handling of configuration changes here here

I handled by encapsulating view's state in specific ViewState class in presenter, and it is easy to test.
public interface BaseViewState {
void saveState(#NonNull Bundle outState);
void restoreState(#Nullable Bundle savedInstanceState);
}
class HomeViewState implements BaseViewState {
static final long NONE_NUM = -1;
static final String STATE_COMIC_NUM = "state_comic_num";
private long comicNum = NONE_NUM;
#Inject
HomeViewState() {
}
#Override
public void saveState(#NonNull Bundle outState) {
outState.putLong(STATE_COMIC_NUM, comicNum);
}
#Override
public void restoreState(#Nullable Bundle savedInstanceState) {
if (savedInstanceState != null) {
comicNum = savedInstanceState.getLong(STATE_COMIC_NUM, NONE_NUM);
}
}
long getComicNumber() {
return comicNum;
}
void setComicNum(long comicNum) {
this.comicNum = comicNum;
}
}
get/set values from viewState in presenter, this helps to keep it updated, as well as presenter stateless.
public class HomePresenter implements HomeContract.Presenter {
private HomeViewState viewState;
HomeViewState getViewState() {
return viewState;
}
#Override
public void loadComic() {
loadComic(viewState.getComicNumber());
}
...
}
in Activity as View should initiate call to save and restore.
public class MainActivity extends BaseActivity implements HomeContract.View {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
homePresenter.getViewState().restoreState(savedInstanceState);
}
#Override
public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
super.onSaveInstanceState(outState, outPersistentState);
homePresenter.getViewState().saveState(outState);
}
...
}

Related

Loading data in ViewModel that have been retrieved in SplashActvity

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.

viewmodel making network call on screen orientation change android

I am new to android architecture components and I am little confused with viewmodel. I am building an app which get a list of items from the server and display as a list in the layout. I have implemented the network call in the Repository class.
Repository.java:
//Get list of top rated movies
public LiveData<NetworkResponse> getTopRatedMovies() {
final MutableLiveData<NetworkResponse> result = new MutableLiveData<>();
ApiService api = retrofit.create(ApiService.class);
Call<MovieData> call = api.getTopRateMovies("api_key");
call.enqueue(new Callback<MovieData>() {
#Override
public void onResponse(Call<MovieData> call, Response<MovieData> response) {
result.postValue(new NetworkResponse(response.body()));
}
#Override
public void onFailure(Call<MovieData> call, Throwable t) {
Log.e(TAG, t.getLocalizedMessage());
result.postValue(new NetworkResponse(t));
}
});
return result;
}
Now in the ViewModel class I am doing this:
public class MovieListViewModel extends ViewModel {
public LiveData<NetworkResponse> result, topRatedMovies;
public LiveData<List<MovieEntity>> favoriteMovies;
private Repository repository;
public MovieListViewModel() {
repository = new Repository(MyApplication.getInstance());
}
public void getTopRatedMovieList() {
topRatedMovies = repository.getTopRatedMovies();
}
}
Now in the MainActivity.java:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
((MyApplication) getApplication()).getComponent().inject(this);
movieListViewModel = ViewModelProviders.of(this).get(MovieListViewModel.class);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new GridLayoutManager(this, 2));
adapter = new MovieListAdapter(this);
movieListViewModel.getTopRatedMovieList();
observeTopRatedMovies();
}
private void observeTopRatedMovies() {
movieListViewModel.topRatedMovies.observe(this, new Observer<NetworkResponse>() {
#Override
public void onChanged(#Nullable NetworkResponse networkResponse) {
if (networkResponse.getPostData() != null) {
Log.e(TAG, "Successful");
topRatedData = networkResponse.getPostData();
adapter.addData(networkResponse.getPostData().getResults());
recyclerView.setAdapter(adapter);
} else {
Log.e(TAG, "failure");
}
}
});
}
Now everything works fine and I am able to see the list. But if I rotate the phone the viewmodel makes the network call again. How can I avoid the network call again on screen orientation change?
You can initialize live data only once. That should be enough:
public class MovieListViewModel extends ViewModel {
public LiveData<NetworkResponse> result, topRatedMovies;
public LiveData<List<MovieEntity>> favoriteMovies;
private Repository repository;
public MovieListViewModel() {
repository = new Repository(MyApplication.getInstance());
topRatedMovies = repository.getTopRatedMovies();
}
}
I suggest you to use headless-fragment design pattern. A headless fragment is a fragment that retain his configuration and it doesn't inflate any xml. If you rotate your app the fragment continue with his logic and configuration and is very useful when you have to do async task or async call (like you in retrofit)
define your fragment:
public class YourFragment extends Fragment {
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true); // <--------- the fragment retain his configuration
}
public void yourLogic(){
// do your logic
}
}
in your MainActivity class create the fragment or get the istance fragment if it already exists:
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_yourHeadLessFragment= (YourFragment) getSupportFragmentManager().findFragmentByTag(HEADLESS_FRAGMENT);
if (_yourHeadLessFragment== null) {
_yourHeadLessFragment= new YourFragment();
_yourHeadLessFragment.setListener(this); // if you want a callback
getSupportFragmentManager().beginTransaction().add(_yourHeadLessFragment, HEADLESS_FRAGMENT).commit();
}
else{
_yourHeadLessFragment.setListener(this); // refresh the callbacks if a rotation happened
}
}
}
you can put network call in the init block of ViewModel

Sharing data between fragments using new architecture component ViewModel

On Last Google IO, Google released a preview of some new arch components, one of which, ViewModel.
In the docs google shows one of the possible uses for this component:
It is very common that two or more fragments in an activity need to
communicate with each other. This is never trivial as both fragments
need to define some interface description, and the owner activity must
bind the two together. Moreover, both fragments must handle the case
where the other fragment is not yet created or not visible.
This common pain point can be addressed by using ViewModel objects.
Imagine a common case of master-detail fragments, where we have a
fragment in which the user selects an item from a list and another
fragment that displays the contents of the selected item.
These fragments can share a ViewModel using their activity scope to
handle this communication.
And shows a implementation example:
public class SharedViewModel extends ViewModel {
private final SavedStateHandle state;
public SharedViewModel(SavedStateHandle state) {
this.state = state;
}
private final MutableLiveData<Item> selected = state.getLiveData("selected");
public void select(Item item) {
selected.setValue(item);
}
public LiveData<Item> getSelected() {
return selected;
}
}
public class MasterFragment extends Fragment {
private SharedViewModel model;
#Override
protected void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
model = new ViewModelProvider(getActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {
model.select(item);
});
}
}
public class DetailFragment extends Fragment {
#Override
protected void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
SharedViewModel model = new ViewModelProvider(getActivity()).get(SharedViewModel.class);
model.getSelected().observe(this, { item ->
// update UI
});
}
}
I was quite excited about the possibility of not needing those interfaces used for fragments to communicate through the activity.
But Google's example does not show exactly how would I call the detail fragment from master.
I'd still have to use an interface that will be implemented by the activity, which will call fragmentManager.replace(...), or there is another way to do that using the new architecture?
Updated on 6/12/2017,
Android Official provide a simple, precise example to example how the ViewModel works on Master-Detail template, you should take a look on it first.Share data between fragments
As #CommonWare, #Quang Nguyen methioned, it is not the purpose for Yigit to make the call from master to detail but be better to use the Middle man pattern. But if you want to make some fragment transaction, it should be done in the activity. At that moment, the ViewModel class should be as static class in Activity and may contain some Ugly Callback to call back the activity to make the fragment transaction.
I have tried to implement this and make a simple project about this. You can take a look it. Most of the code is referenced from Google IO 2017, also the structure.
https://github.com/charlesng/SampleAppArch
I do not use Master Detail Fragment to implement the component, but the old one ( communication between fragment in ViewPager.) The logic should be the same.
But I found something is important using these components
What you want to send and receive in the Middle man, they should be sent and received in View Model only
The modification seems not too much in the fragment class. Since it only change the implementation from "Interface callback" to "Listening and responding ViewModel"
View Model initialize seems important and likely to be called in the activity.
Using the MutableLiveData to make the source synchronized in activity only.
1.Pager Activity
public class PagerActivity extends AppCompatActivity {
/**
* The pager widget, which handles animation and allows swiping horizontally to access previous
* and next wizard steps.
*/
private ViewPager mPager;
private PagerAgentViewModel pagerAgentViewModel;
/**
* The pager adapter, which provides the pages to the view pager widget.
*/
private PagerAdapter mPagerAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pager);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
mPager = (ViewPager) findViewById(R.id.pager);
mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
mPager.setAdapter(mPagerAdapter);
pagerAgentViewModel = new ViewModelProvider(this).get(PagerAgentViewModel.class);
pagerAgentViewModel.init();
}
/**
* A simple pager adapter that represents 5 ScreenSlidePageFragment objects, in
* sequence.
*/
private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
...Pager Implementation
}
}
2.PagerAgentViewModel (It deserved a better name rather than this)
public class PagerAgentViewModel extends ViewModel {
private final SavedStateHandle state;
private final MutableLiveData<String> messageContainerA;
private final MutableLiveData<String> messageContainerB;
public PagerAgentViewModel(SavedStateHandle state) {
this.state = state;
messageContainerA = state.getLiveData("Default Message");
messageContainerB = state.getLiveData("Default Message");
}
public void sendMessageToB(String msg)
{
messageContainerB.setValue(msg);
}
public void sendMessageToA(String msg)
{
messageContainerA.setValue(msg);
}
public LiveData<String> getMessageContainerA() {
return messageContainerA;
}
public LiveData<String> getMessageContainerB() {
return messageContainerB;
}
}
3.BlankFragmentA
public class BlankFragmentA extends Fragment {
private PagerAgentViewModel viewModel;
public BlankFragmentA() {
// Required empty public constructor
}
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel = new ViewModelProvider(getActivity()).get(PagerAgentViewModel.class);
textView = (TextView) view.findViewById(R.id.fragment_textA);
// set the onclick listener
Button button = (Button) view.findViewById(R.id.btnA);
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
viewModel.sendMessageToB("Hello B");
}
});
//setup the listener for the fragment A
viewModel.getMessageContainerA().observe(getViewLifecycleOwner(), new Observer<String>() {
#Override
public void onChanged(#Nullable String msg) {
textView.setText(msg);
}
});
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_blank_a, container, false);
return view;
}
}
4.BlankFragmentB
public class BlankFragmentB extends Fragment {
public BlankFragmentB() {
// Required empty public constructor
}
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel = new ViewModelProvider(getActivity()).get(PagerAgentViewModel.class);
textView = (TextView) view.findViewById(R.id.fragment_textB);
//set the on click listener
Button button = (Button) view.findViewById(R.id.btnB);
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
viewModel.sendMessageToA("Hello A");
}
});
//setup the listener for the fragment B
viewModel.getMessageContainerB().observe(getViewLifecycleOwner(), new Observer<String>() {
#Override
public void onChanged(#Nullable String msg) {
textView.setText(msg);
}
});
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_blank_b, container, false);
return view;
}
}
As written in the official Google tutorial now you may obtain a shared view model with by activityViewModels()
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: SharedViewModel by activityViewModels()
I have found a similar solution as others according to google codelabs example.
I have two fragments where one of them wait for an object change in the other and continues its process with updated object.
for this approach you will need a ViewModel class as below:
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.ViewModel;
import yourPackage.YourObjectModel;
public class SharedViewModel extends ViewModel {
public MutableLiveData<YourObjectModel> item = new MutableLiveData<>();
public YourObjectModel getItem() {
return item.getValue();
}
public void setItem(YourObjectModel item) {
this.item.setValue(item);
}
}
and the listener fragment should look like this:
public class ListenerFragment extends Fragment{
private SharedViewModel model;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
model.item.observe(getActivity(), new Observer<YourObjectModel>(){
#Override
public void onChanged(#Nullable YourObjectModel updatedObject) {
Log.i(TAG, "onChanged: recieved freshObject");
if (updatedObject != null) {
// Do what you want with your updated object here.
}
}
});
}
}
finally, the updater fragment can be like this:
public class UpdaterFragment extends DialogFragment{
private SharedViewModel model;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
}
// Call this method where it is necessary
private void updateViewModel(YourObjectModel yourItem){
model.setItem(yourItem);
}
}
It is good to mention that the updater fragment can be any form of fragments(not DialogFragment only) and for using these architecture components you should have these lines of code in your app build.gradle file. source
dependencies {
def lifecycle_version = "1.1.1"
implementation "android.arch.lifecycle:extensions:$lifecycle_version"
}
Before you are using a callback which attaches to Activity which is considered as a container.
That callback is a middle man between two Fragments.
The bad things about this previous solution are:
Activity has to carry the callback, it means a lot of work for
Activity.
Two Fragments are coupled tightly, it is difficult to update or change logic later.
With the new ViewModel (with support of LiveData), you have an elegant solution. It now plays a role of middle man which you can attach its lifecycle to Activity.
Logic and data between two Fragments now lay out in ViewModel.
Two Fragment gets data/state from ViewModel, so they do not need to know each other.
Besides, with the power of LiveData, you can change detail Fragment based on changes of master Fragment in reactive approach instead of previous callback way.
You now completely get rid of callback which tightly couples to both Activity and related Fragments.
I highly recommend you through Google's code lab. In step 5, you can find an nice example about this.
I implemented something similar to what you want, my viewmodel contains LiveData object that contains Enum state, and when you want to change the fragment from master to details (or in reverse) you call ViewModel functions that changing the livedata value, and activity know to change the fragment because it is observing livedata object.
TestViewModel:
public class TestViewModel extends ViewModel {
private MutableLiveData<Enums.state> mState;
public TestViewModel() {
mState=new MutableLiveData<>();
mState.setValue(Enums.state.Master);
}
public void onDetail() {
mState.setValue(Enums.state.Detail);
}
public void onMaster() {
mState.setValue(Enums.state.Master);
}
public LiveData<Enums.state> getState() {
return mState;
}
}
Enums:
public class Enums {
public enum state {
Master,
Detail
}
}
TestActivity:
public class TestActivity extends LifecycleActivity {
private ActivityTestBinding mBinding;
private TestViewModel mViewModel;
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding=DataBindingUtil.setContentView(this, R.layout.activity_test);
mViewModel=ViewModelProviders.of(this).get(TestViewModel.class);
mViewModel.getState().observe(this, new Observer<Enums.state>() {
#Override
public void onChanged(#Nullable Enums.state state) {
switch(state) {
case Master:
setMasterFragment();
break;
case Detail:
setDetailFragment();
break;
}
}
});
}
private void setMasterFragment() {
MasterFragment masterFragment=MasterFragment.newInstance();
getSupportFragmentManager().beginTransaction().replace(R.id.frame_layout, masterFragment,"MasterTag").commit();
}
private void setDetailFragment() {
DetailFragment detailFragment=DetailFragment.newInstance();
getSupportFragmentManager().beginTransaction().replace(R.id.frame_layout, detailFragment,"DetailTag").commit();
}
#Override
public void onBackPressed() {
switch(mViewModel.getState().getValue()) {
case Master:
super.onBackPressed();
break;
case Detail:
mViewModel.onMaster();
break;
}
}
}
MasterFragment:
public class MasterFragment extends Fragment {
private FragmentMasterBinding mBinding;
public static MasterFragment newInstance() {
MasterFragment fragment=new MasterFragment();
return fragment;
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
mBinding=DataBindingUtil.inflate(inflater,R.layout.fragment_master, container, false);
mBinding.btnDetail.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
final TestViewModel viewModel=ViewModelProviders.of(getActivity()).get(TestViewModel.class);
viewModel.onDetail();
}
});
return mBinding.getRoot();
}
}
DetailFragment:
public class DetailFragment extends Fragment {
private FragmentDetailBinding mBinding;
public static DetailFragment newInstance() {
DetailFragment fragment=new DetailFragment();
return fragment;
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
mBinding=DataBindingUtil.inflate(inflater,R.layout.fragment_detail, container, false);
mBinding.btnMaster.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
final TestViewModel viewModel=ViewModelProviders.of(getActivity()).get(TestViewModel.class);
viewModel.onMaster();
}
});
return mBinding.getRoot();
}
}
I end up using the own ViewModel to hold up the listener that will trigger the Activity method. Similar to the old way but as I said, passing the listener to ViewModel instead of the fragment. So my ViewModel looked like this:
public class SharedViewModel<T> extends ViewModel {
private final MutableLiveData<T> selected = new MutableLiveData<>();
private OnSelectListener<T> listener = item -> {};
public interface OnSelectListener <T> {
void selected (T item);
}
public void setListener(OnSelectListener<T> listener) {
this.listener = listener;
}
public void select(T item) {
selected.setValue(item);
listener.selected(item);
}
public LiveData<T> getSelected() {
return selected;
}
}
in StepMasterActivity I get the ViewModel and set it as a listener:
StepMasterActivity.class:
SharedViewModel stepViewModel = ViewModelProviders.of(this).get("step", SharedViewModel.class);
stepViewModel.setListener(this);
...
#Override
public void selected(Step item) {
Log.d(TAG, "selected: "+item);
}
...
In the fragment I just retrieve the ViewModel
stepViewModel = ViewModelProviders.of(getActivity()).get("step", SharedViewModel.class);
and call:
stepViewModel.select(step);
I tested it superficially and it worked. As I go about implementing the other features related to this, I will be aware of any problems that may occur.
For those using Kotlin out there try the following approach:
Add the androidx ViewModel and LiveData libraries to your gradle file
Call your viewmodel inside the fragment like this:
class MainFragment : Fragment() {
private lateinit var viewModel: ViewModel
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// kotlin does not have a getActivity() built in method instead we use activity, which is null-safe
activity?.let {
viemModel = ViewModelProvider(it).get(SharedViewModel::class.java)
}
}
}
The above method is a good practice since it will avoid crashes due to null pointer exceptions
Edit: As btraas complemented: activity is compiled into getActivity() which is marked as #Nullable in the android SDK. activity and getActivity() are both accessible and equivalent.
You can set values from Detail Fragment to Master Fragment like this
model.selected.setValue(item)

Android unit testing fragment with roboletric in MPV application

I am reinventing my app using a classic MVP approach. In order to to this I read many many articles and tutorials, and what I came out with is that the best way is to :
create an interface for the presenter and one for the view
make fragments and activities implements view interfaces
create an implementation of the presenter interface, which takes in the constructor an instance of the the view it manages, and hold a reference to the presenter inside the view's implementation.
So I have created this classes
VIEW INTERFACE
public interface SignupEmailView extends BaseView {
void fillEmail(String email);
void onEmailInvalid(String error);
void onDataValidated();
}
PRESENTER INTERFACE
public interface SignupEmailPresenter {
void initData(Bundle bundle);
void validateData(String email);
}
VIEW IMPLEMENTATION
public class FrSignup_email extends BaseSignupFragmentMVP implements IBackHandler, SignupEmailView {
public static String PARAM_EMAIL = "param_email";
#Bind(R.id.signup_step2_new_scrollview)
ScrollView mScrollview;
#Bind(R.id.signup_step2_new_lblTitle)
SuperLabel mLblTitle;
#Bind(R.id.signup_step2_new_lblSubtitle)
TextView mLblSubtitle;
#Bind(R.id.signup_step2_new_txtEmail)
EditText mTxtEmail;
#Bind(R.id.signup_step2_new_btnNext)
Button mBtnNext;
protected SignupActivityView mActivity;
SignupEmailPresenter mPresenter;
public FrSignup_email() {
// Required empty public constructor
}
public static FrSignup_email newInstance(String email) {
FrSignup_email fragment = new FrSignup_email();
Bundle b = new Bundle();
b.putString(PARAM_EMAIL, email);
fragment.setArguments(b);
return fragment;
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mActivity = (SignupActivityView) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement IResetPasswordBridge");
}
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = loadView(inflater, container, savedInstanceState, R.layout.fragment_signup_email);
mPresenter = new SignupEmailPresenterImpl(this);
ButterKnife.bind(this, view);
return view;
}
#Override
public final void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
applyCircularReveal();
mPresenter.initData(this.getArguments());
mTxtEmail.setImeOptions(EditorInfo.IME_ACTION_NEXT);
mTxtEmail.setOnEditorActionListener(new TextView.OnEditorActionListener() {
#Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_NEXT) {
mPresenter.validateData(mTxtEmail.getText().toString());
return true;
}
return false;
}
});
mTxtEmail.setOnTouchListener(new OnTouchCompoundDrawableListener_NEW(mTxtEmail, new OnTouchCompoundDrawableListener_NEW.OnTouchCompoundDrawable() {
#Override
public void onTouch() {
mTxtEmail.setText("");
}
}));
mBtnNext.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
mPresenter.validateData(mTxtEmail.getText().toString());
}
});
}
#Override
public void fillEmail(String email) {
mTxtEmail.setText(email);
}
#Override
public void onEmailInvalid(String error) {
displayError(error);
}
#Override
public void onDataValidated() {
changeFieldToValid(mTxtEmail);
setEmail(mTxtEmail.getText().toString());
// the activity shows the next fragment
mActivity.onEmailValidated();
}
#Override
public boolean doBack() {
if (!isLoading()) {
mActivity.onEmailBack();
}
return true;
}
#Override
public void displayError(String error) {
changeFieldToInvalid(mTxtEmail);
mLblSubtitle.setText(error);
mLblSubtitle.setTextColor(ContextCompat.getColor(getActivity(), R.color.field_error));
}
}
PRESENTER IMPLEMENTATION
public class SignupEmailPresenterImpl implements SignupEmailPresenter {
private SignupEmailView mView;
public SignupEmailPresenterImpl(SignupEmailView view) {
mView = view;
}
#Override
public void initData(Bundle bundle) {
if (bundle != null) {
mView.fillEmail(bundle.getString(FrSignup_email.PARAM_EMAIL));
}
}
#Override
public void validateData(String password) {
ValidationUtils_NEW.EmailStatus status = ValidationUtils_NEW.validateEmail(password);
if (status != ValidationUtils_NEW.EmailStatus.VALID) {
mView.onEmailInvalid(ValidationUtils_NEW.getEmailErrorMessage(status));
} else {
mView.onDataValidated();
}
}
}
Now the fragment is held by an activity which implements this view interface and has its own presenter
public interface SignupActivityView extends BaseView {
void onEmailValidated();
void onPhoneNumberValidated();
void onPasswordValidated();
void onUnlockCodeValidated();
void onResendCodeClick();
void onEmailBack();
void onPhoneNumberBack();
void onPasswordBack();
void onConfirmCodeBack();
void onSignupRequestSuccess(boolean resendingCode);
void onSignupRequestFailed(String errorMessage);
void onTokenCreationFailed();
void onUnlockSuccess();
void onUnlockError(String errorMessage);
void showTermsAndConditions();
void hideTermsAndConditions();
}
My idea is to have a unit test for each project unit, so for each view and presenter implementation I want a unit test, so I want to unit test my fragment with roboletric, and for example I want to test that if I click the "NEXT" button and the email is correct, the hosting Activity's onEmailValidated()method is called. This is my test class
public class SignupEmailViewTest {
private SignupActivity_NEW mActivity;
private SignupActivity_NEW mSpyActivity;
private FrSignup_email mFragment;
private FrSignup_email mSpyFragment;
private Context mContext;
#Before
public void setUp() {
final Context context = RuntimeEnvironment.application.getApplicationContext();
this.mContext = context;
mActivity = Robolectric.buildActivity(SignupActivity_NEW.class).create().visible().get();
mSpyActivity = spy(mActivity);
mFragment = FrSignup_email.newInstance("");
mSpyFragment =spy(mFragment);
mSpyActivity.getFragmentManager()
.beginTransaction()
.replace(R.id.signupNew_fragmentHolder, mSpyFragment)
.commit();
mSpyActivity.getFragmentManager().executePendingTransactions();
}
#Test
public void testEmailValidation() {
assertTrue(mSpyActivity.findViewById(R.id.signup_step2_new_lblTitle).isShown());
assertTrue(mSpyActivity.findViewById(R.id.signup_step2_new_lblSubtitle).isShown());
mSpyActivity.findViewById(R.id.signup_step2_new_btnNext).performClick();
assertTrue(((SuperLabel) mSpyActivity.findViewById(R.id.signup_step2_new_lblSubtitle)).getText().equals(mContext.getString(R.string.email_empty)));
((EditText) mSpyActivity.findViewById(R.id.signup_step2_new_txtEmail)).setText("aaa#bbb.ccc");
mSpyActivity.findViewById(R.id.signup_step2_new_btnNext).performClick();
verify(mSpyFragment).onDataValidated();
verify(mSpyActivity).onEmailValidated();
}
}
everything works well, is just the last verify which doesn't work. Note that the previous verify works, so onEmailValidated is called for sure.
Aside from this specific case, I have some point to discuss:
If with roboeletric I am forced to use an activity to instantiate a fragment, how can I test the fragment in complete isolation (which would be the unit tests goal)? I mean, if I use Robolectric.setupActivity(MyActivity.class) and the activity instantiates somewhere a fragment, it will load the activity and the fragment, which is good, but what if the activity manages a flow of fragments? How can I test the second or third fragment without manually navigating to it? Someone can say to use a dummy activity and use FragmentTestUtil.startFragment, but what in the fragment's onAttach() method is implemented the bridging with the parent activity? Is it me going on the wrong way or are this problems still unsolved?
thanks
Actually you don't even require Roboelectric to do any of those tests.
If each fragment/activity implements a different view interface you could implement fake views and instantiate those instead of the activity/fragment. In this way you could have isolated tests.
If you don't want to implement all the methods of the view interface you could use Mockito and stub only the ones that your unit test requires.
Let me know if you need sample code.

Adding Functionality to both Android's Activity (parent class) and FragmentActivity (child class) in best coding style (minimal duplication)

In Android Library, FragmentActivity extends Activity. I would like to add a few methods, and override some methods, of the original Activity.
import android.app.Activity
public class Activiti extends Activity {
public void myNewMethod() { ... }
}
Because of the original hierarchy, FragmentActivity extends Activity, myNewMethod() should also be present in my library's FragmentActiviti
import android.support.v4.app.FragmentActivity;
public abstract class FragmentActiviti extends FragmentActivity {
public void myNewMethod() { ... }
}
But this will lead to a duplication of code, which i do not want this happens. Is there a way to avoid this duplication?
Edit: Usage scenario
Activiti.java
public abstract class Activiti extends Activity {
private int current_orientation = Configuration.ORIENTATION_UNDEFINED; // ORIENTATION_UNDEFINED = 0
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
current_orientation = this.getResources().getConfiguration().orientation;
}
protected boolean isDevicePortrait() { return current_orientation == Configuration.ORIENTATION_PORTRAIT; }
}
FragmentActiviti.java
public abstract class FragmentActiviti extends FragmentActivity {
/* This onCreate() can be omitted. Just putting here explicitly. */
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
protected void someUtilsForFragments() { /* not used yet */ }
}
E_fragtest_06.java
public class E_fragtest_06 extends FragmentActiviti {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
System.out.printf(isDevicePortrait()); // this NOT WORK for now
}
}
Edit 2: Try using Util class
i think using the Decorator Class would be the most nicest way to solve this problem (no duplication of code). But the Decorator Pattern is just a bit hard (or impossible) to apply on Android Activity scenario.
i try implementing #hazzik's approach, but i still experience some problems.
ActivityUtil.java
public abstract class ActivityUtil {
private int current_orientation = Configuration.ORIENTATION_UNDEFINED; // ORIENTATION_UNDEFINED = 0
public void onCreate(Activity activity, Bundle savedInstanceState) {
activity.onCreate(savedInstanceState);
current_orientation = activity.getResources().getConfiguration().orientation;
}
public boolean isDevicePortrait() { return current_orientation == Configuration.ORIENTATION_PORTRAIT; }
}
Activiti.java
public class Activiti extends Activity {
private ActivityUtil activityUtil;
#Override
public void onCreate(Bundle savedInstanceState) {
activityUtil.onCreate(this, savedInstanceState);
}
protected boolean isDevicePortrait() { return activityUtil.isDevicePortrait(); }
}
FragmentActiviti.java
public abstract class FragmentActiviti extends FragmentActivity {
private ActivityUtil activityUtil;
#Override
public void onCreate(Bundle savedInstanceState) {
activityUtil.onCreate(this, savedInstanceState);
}
protected boolean isDevicePortrait() { return activityUtil.isDevicePortrait(); }
}
In ActivityUtil.onCreate(), activity.onCreate(savedInstanceState); is causing this compile error:
The method onCreate(Bundle) from the type Activity is not visible.
If i change Activity to Activiti:
public abstract class ActivityUtil {
public void onCreate(Activiti activity, Bundle savedInstanceState) { ... }
...
}
It will lead to another compile error in FragmentActiviti.onCreate()'s activityUtil.onCreate():
The method onCreate(Activiti, Bundle) in the type ActivityUtil is not applicable for the arguments (FragmentActiviti, Bundle)
i understand why those errors occur. But i just don't know how to avoid them.
To thanks all the guys who have been contributing to this question, especially #flup for guiding me about the Decorator Pattern, #hazzik and #donramos for your extensive efforts, i m here posting
My enhanced Android's Activity and FragmentActivity classes.
If you are also developing Android applications, i hope my codes could help you guys in some ways :-)
ActivityCore.java
package xxx.android;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.res.Configuration;
import android.os.Bundle;
public final class ActivityCore {
public interface ActivityCallbackInterface {
public void onCreateCallback(Bundle savedInstanceState);
public void onBeforeSaveInstanceState(Bundle outState);
public void onSaveInstanceStateCallback(Bundle outState);
}
private final Activity activity;
/**
* This current_orientation variable should be once set, never changed during the object life-cycle.
* But Activity.getResources() is not yet ready upon the object constructs.
* That's why THIS CLASS is wholly responsible to maintain THIS VARIABLE UNCHANGED.
*/
private int current_orientation = Configuration.ORIENTATION_UNDEFINED; // ORIENTATION_UNDEFINED = 0
public ActivityCore(Activity activity) { this.activity = activity; }
public void onCreate(Bundle savedInstanceState) {
((ActivityCallbackInterface) activity).onCreateCallback(savedInstanceState);
current_orientation = activity.getResources().getConfiguration().orientation;
}
public void onSaveInstanceState(Bundle outState) {
/**
* THIS is the best ever place i have found, to unload unwanted Fragments,
* thus prevent re-creating of un-needed Fragments in the next state of Activity.
* (state e.g. Portrait-to-Landscape, or Landscape-to-Portrait)
*
* The KEY is to do it BEFORE super.onSaveInstanceState()
* (my guess for this reason is, in super.onSaveInstanceState(),
* it saves the layout hierarchy, thus saved the Fragments into the Bundle also.
* Thus restored.
* Note that Fragments NOT IN LAYOUT, having ONLY TAGS, are also restored.)
*/
((ActivityCallbackInterface) activity).onBeforeSaveInstanceState(outState);
((ActivityCallbackInterface) activity).onSaveInstanceStateCallback(outState);
}
public int getCurrentOrientation() { return current_orientation; }
public boolean isDevicePortrait() { return current_orientation == Configuration.ORIENTATION_PORTRAIT; }
public boolean isDeviceLandscape() { return current_orientation == Configuration.ORIENTATION_LANDSCAPE; }
public boolean isNewDevicePortrait() { return activity.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; }
public boolean isNewDeviceLandscape() { return activity.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; }
public boolean isPortrait2Landscape() { return isDevicePortrait() && isNewDeviceLandscape(); }
public boolean isLandscape2Portrait() { return isDeviceLandscape() && isNewDevicePortrait(); }
public String describeCurrentOrientation() { return describeOrientation(current_orientation); }
public String getCurrentOrientationTag() { return getOrientationTag(current_orientation); }
public String describeNewOrientation() { return describeOrientation(activity.getResources().getConfiguration().orientation); }
public String getNewOrientationTag() { return getOrientationTag(activity.getResources().getConfiguration().orientation); }
private String describeOrientation(final int orientation) {
switch (orientation) {
case Configuration.ORIENTATION_UNDEFINED: return "ORIENTATION_UNDEFINED"; // 0
case Configuration.ORIENTATION_PORTRAIT: return "ORIENTATION_PORTRAIT"; // 1
case Configuration.ORIENTATION_LANDSCAPE: return "ORIENTATION_LANDSCAPE"; // 2
case Configuration.ORIENTATION_SQUARE: return "ORIENTATION_SQUARE"; // 3
default: return null;
}
}
#SuppressLint("DefaultLocale")
private String getOrientationTag(final int orientation) {
return String.format("[%d:%s]", orientation, describeOrientation(orientation).substring(12, 16).toLowerCase());
}
}
Activity.java
package xxx.android.app;
import xxx.android.ActivityCore;
import xxx.android.ActivityCore.ActivityCallbackInterface;
import android.os.Bundle;
public abstract class Activity extends android.app.Activity implements ActivityCallbackInterface {
private final ActivityCore activityCore;
public Activity() { super(); activityCore = new ActivityCore(this); }
#Override
protected void onCreate(Bundle savedInstanceState) { activityCore.onCreate(savedInstanceState); }
#Override public void onCreateCallback(Bundle savedInstanceState) { super.onCreate(savedInstanceState); }
#Override
public void onBeforeSaveInstanceState(Bundle outState) {} // Optionally: let child class override
#Override
protected void onSaveInstanceState(Bundle outState) { activityCore.onSaveInstanceState(outState); }
#Override public void onSaveInstanceStateCallback(Bundle outState) { super.onSaveInstanceState(outState); }
public final int getCurrentOrientation() { return activityCore.getCurrentOrientation(); }
public final boolean isDevicePortrait() { return activityCore.isDevicePortrait(); }
public final boolean isDeviceLandscape() { return activityCore.isDeviceLandscape(); }
public final boolean isNewDevicePortrait() { return activityCore.isNewDevicePortrait(); }
public final boolean isNewDeviceLandscape() { return activityCore.isNewDeviceLandscape(); }
public final boolean isPortrait2Landscape() { return activityCore.isPortrait2Landscape(); }
public final boolean isLandscape2Portrait() { return activityCore.isLandscape2Portrait(); }
public final String describeCurrentOrientation() { return activityCore.describeCurrentOrientation(); }
public final String getCurrentOrientationTag() { return activityCore.getCurrentOrientationTag(); }
public final String describeNewOrientation() { return activityCore.describeNewOrientation(); }
public final String getNewOrientationTag() { return activityCore.getNewOrientationTag(); }
}
FragmentActivity.java
package xxx.android.support.v4.app;
import xxx.android.ActivityCore;
import xxx.android.ActivityCore.ActivityCallbackInterface;
import android.os.Bundle;
public abstract class FragmentActivity extends android.support.v4.app.FragmentActivity implements ActivityCallbackInterface {
private final ActivityCore activityCore;
public FragmentActivity() { super(); activityCore = new ActivityCore(this); }
#Override
protected void onCreate(Bundle savedInstanceState) { activityCore.onCreate(savedInstanceState); }
#Override public void onCreateCallback(Bundle savedInstanceState) { super.onCreate(savedInstanceState); }
#Override
public void onBeforeSaveInstanceState(Bundle outState) {} // Optionally: let child class override
#Override
protected void onSaveInstanceState(Bundle outState) { activityCore.onSaveInstanceState(outState); }
#Override public void onSaveInstanceStateCallback(Bundle outState) { super.onSaveInstanceState(outState); }
public final int getCurrentOrientation() { return activityCore.getCurrentOrientation(); }
public final boolean isDevicePortrait() { return activityCore.isDevicePortrait(); }
public final boolean isDeviceLandscape() { return activityCore.isDeviceLandscape(); }
public final boolean isNewDevicePortrait() { return activityCore.isNewDevicePortrait(); }
public final boolean isNewDeviceLandscape() { return activityCore.isNewDeviceLandscape(); }
public final boolean isPortrait2Landscape() { return activityCore.isPortrait2Landscape(); }
public final boolean isLandscape2Portrait() { return activityCore.isLandscape2Portrait(); }
public final String describeCurrentOrientation() { return activityCore.describeCurrentOrientation(); }
public final String getCurrentOrientationTag() { return activityCore.getCurrentOrientationTag(); }
public final String describeNewOrientation() { return activityCore.describeNewOrientation(); }
public final String getNewOrientationTag() { return activityCore.getNewOrientationTag(); }
}
Lastly, i really have to thanks you guys are being so so so helpful and keep updating the solving progress with me! You all are the key persons who make stackoverflow a perfect site for programmers. Should you spot any problems in my codes, or any rooms for improvements, please do not hesitate to help me again :-)
Some improvements?
It is because onBeforeSaveInstanceState() is implemented upon usage, all the three classes need to keep abstract. This leads to a duplication of the member variable current_orientation. If current_orientation could be put into class ActivityBase, or grouping it into somewhere else, it would be a lot nicer!
stupid me. i have fixed it :-)
For my point of view the best solution here is to delegate logic to some class, let's call it CustomActivityLogic.
Also you need to create common interface (CustomActivity) for your activities if you want to access some data or methods of activity classes from your logic class.
To call protected virtual overridden methods there are two solutions:
call method of supper from overridden method
make a new method in subclass and call super method from this new method. Call new method from shared logic.
CustomActivity.java
public interface CustomActivity {
void someMethod();
}
Activiti.java
import android.app.Activity
public class Activiti
extends Activity
implements CustomActivity {
private CustomActivityLogic logic = new CustomActivityLogic();
public void someMethod() { /***/ }
public void myNewMethod() { logic.myNewMethod(this); }
#Override
protected void onCreate(Bundle savedInstanceState) {
logic.onCreate(this, savedInstanceState); // call shared logic
super.onCreate(savedInstanceState); // call super
}
}
FragmentActivitii.java
import android.support.v4.app.FragmentActivity;
public class FragmentActivitii
extends FragmentActivity
implements CustomActivity {
private CustomActivityLogic logic = new CustomActivityLogic();
public void someMethod() { /***/ }
public void myNewMethod() { logic.myNewMethod(this); }
#Override
protected void onCreate(Bundle savedInstanceState) {
logic.onCreate(this, savedInstanceState); // call shared logic
super.onCreate(savedInstanceState); // call super
}
}
CustomActivityLogic.java
public class CustomActivityLogic {
public void myNewMethod(CustomActivity activity) { /*...*/ }
public void onCreate(Activity activity, Bundle savedInstanceState) {
/* shared creation logic */
}
}
Approach with making onCreate available to call from outside via CustomActivity interface
CustomActivity.java
public interface CustomActivity {
void someMethod();
void onCreateSuper(Bundle savedInstanceState);
}
Activiti.java
import android.app.Activity
public class Activiti
extends Activity
implements CustomActivity {
private CustomActivityLogic logic = new CustomActivityLogic();
public void someMethod() { /***/ }
public void myNewMethod() { logic.myNewMethod(this); }
#Override
protected void onCreate(Bundle savedInstanceState) {
logic.onCreate(this, savedInstanceState); // call shared logic
}
public void onCreateSuper(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); // call super
}
}
FragmentActivitii.java
import android.support.v4.app.FragmentActivity;
public class FragmentActivitii
extends FragmentActivity
implements CustomActivity {
private CustomActivityLogic logic = new CustomActivityLogic();
public void someMethod() { /***/ }
public void myNewMethod() { logic.myNewMethod(this); }
#Override
protected void onCreate(Bundle savedInstanceState) {
logic.onCreate(this, savedInstanceState); // call shared logic
}
public void onCreateSuper(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); // call super
}
}
CustomActivityLogic.java
public class CustomActivityLogic {
public void myNewMethod(CustomActivity activity) { /*...*/ }
public void onCreate(CustomActivity activity, Bundle savedInstanceState) {
/* shared creation logic */
activity.onCreateSuper(savedInstanceState); // call-back super
}
}
You wish to add helper methods that help keep track of the orientation. I'd think this is not quite big enough to warrant the creation of a subclass.
Put them in a helper class instead:
public class OrientationHelper {
private Activity activity;
private int current_orientation;
public OrientationHelper(Activity activity){
this.activity = activity;
orientation = Configuration.ORIENTATION_UNDEFINED;
}
public int getNewOrientation() {
return activity.getResources().getConfiguration().orientation;
}
// call this when you wish to update current_orientation
public void updateOrientation() {
current_orientation = getNewOrientation();
}
public int getCurrentOrientation() {
return current_orientation;
}
public boolean isDevicePortrait() {
return current_orientation == Configuration.ORIENTATION_PORTRAIT;
}
public boolean isDeviceLandscape() {
return current_orientation == Configuration.ORIENTATION_LANDSCAPE;
}
public boolean isNewDevicePortrait() {
return getCurrentOrientation() == Configuration.ORIENTATION_PORTRAIT;
}
public boolean isNewDeviceLandscape() {
return getCurrentOrientation() == Configuration.ORIENTATION_LANDSCAPE;
}
public boolean isPortrait2Landscape() {
return isDevicePortrait() && isNewDeviceLandscape();
}
public boolean isLandscape2Portrait() {
return isDeviceLandscape() && isNewDevicePortrait();
}
public String describeCurrentOrientation() {
return describeOrientation(current_orientation);
}
public String describeNewOrientation() {
return describeOrientation(getNewOrientation());
}
private String describeOrientation(int current_orientation) {
switch (current_orientation) {
case Configuration.ORIENTATION_UNDEFINED:
return "ORIENTATION_UNDEFINED";
case Configuration.ORIENTATION_PORTRAIT:
return "ORIENTATION_PORTRAIT";
case Configuration.ORIENTATION_LANDSCAPE:
return "ORIENTATION_LANDSCAPE";
case Configuration.ORIENTATION_SQUARE:
return "ORIENTATION_SQUARE";
default: return null;
}
}
}
In those activities that work with orientation (and only those), you can instantiate the OrientationHelper and call updateOrientation() in select places.
The other bit of code, that organizes the saving of the instance state, I would not put in a different class just so that you can reuse it. Because this is not where one would expect modifications to state saving to occur and therefore it might get overlooked. (It took me a bit of scrolling around to figure out what it's supposed to do.)
I think the most readable way to go about that is to write it out explicitly in each Activity where you use it.
One last thing to consider is that the Sherlock Actionbar already extends Activity. And rightly so, I think. But this means that you'll occasionally run into trouble if you extend Activity too.
How about using the Decorator Pattern? Unfortunately, this will require you to delegate all of the existing methods, or whichever ones are necessary for your purpose.
public class ActivityDecorator extends Activity
{
private Activity RealActivity;
public ActivityDecorator(Activity _realActivity)
{
RealActivity = _realActivity;
}
public void myNewMethod() { ... } // this exposes the added/new functionality
// unfortunately for old functionality you need to delegate
public void oldMethod() { RealActivity.oldMethod(); }
}
However, once you've done this once for the ActivityProxy class, you can construct instances of ActivityDecorator with types that derive Activity such as FragmentActivity in your case. E.g.
ActivityDecorator decorator = new ActivityDecorator(new FragmentActivity());
Your design problem is one of the issues addressed by the upcoming Java 8 virtual extensions. See URL below for more details:
http://java.dzone.com/articles/java-8-virtual-extension
In the meantime, there is no easy way. A decorator class will not work, instead implement a utility class that will be called by both of your classes:
EDITED BASED ON NEW INFO:
/** NOTE: cannot be abstract class **/
public class ActivitiBase {
private int current_orientation = Configuration.ORIENTATION_UNDEFINED; // ORIENTATION_UNDEFINED = 0
private Activity activity;
public void ActivitiBase(Activity activity) {
this.activity = activity;
}
public void onCreate(Bundle savedInstanceState) {
current_orientation = activity.getResources().getConfiguration().orientation;
}
public boolean isDevicePortrait() { return current_orientation ==
Configuration.ORIENTATION_PORTRAIT; }
}
public void myNewMethod() { ... }
}
Activiti class:
public class Activiti extends Activity {
private ActiviBase activitiBase;
public Activiti() {
activitiBase = new ActiviBase(this);
}
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activitiBase.onCreate(savedInstanceState);
}
public void myNewMethod() {
activitiBase.myNewMethod();
}
}
FrameActiviti class:
public class FrameActiviti extends FrameActivity {
private ActiviBase activitiBase;
public FrameActiviti() {
activitiBase = new ActiviBase(this);
}
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activitiBase.onCreate(savedInstanceState);
}
public void myNewMethod() {
activitiBase.myNewMethod();
}
}

Categories

Resources