Memory leak with Firebase Android PhoneAuthProvider - java

Firebase -> PhoneAuthProvider -> VerifyPhoneNumber is leaking. I believe, it might be OnVerificationStateChangedCallbacks, which we are sending on call to verifyPhoneNumber.
Steps to reproduce:
Launch the app
Select "PhoneAuthActivity" for phone based authentication
Send Phone Number.
Click back.
When clicking back, the leaked memory appear
Does someone have the same problem? any solution?
public void FirebasePhoneUser(String phoneNumber) {
mCallback = new PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
#Override
public void onVerificationCompleted(PhoneAuthCredential phoneAuthCredential) {
Log.d("Completed","");
}
#Override
public void onVerificationFailed(FirebaseException e) {
Log.d("Error","");
}
#Override
public void onCodeSent(String verificationId,
PhoneAuthProvider.ForceResendingToken forceResendingToken) {
Log.d("onCodeSent", "");
}
};
phoneAuthProvider = PhoneAuthProvider.getInstance();
phoneAuthProvider.verifyPhoneNumber(
phoneNumber,
30,
TimeUnit.SECONDS,
TaskExecutors.MAIN_THREAD,
mCallback
);
}

Given that API is terrible and there's no option to unsubscribe, you have several options to work this around.
Proxy or Decorator. You create another OnVerificationStateChangedCallbacks which delegates method calls to another instance:
// this class must be either top-level or 'static'!
public /*static*/ final class DelegatingVerificationStateCallbacks
extends PhoneAuthProvider.OnVerificationStateChangedCallbacks
implements Closeable {
#Nullable private PhoneAuthProvider.OnVerificationStateChangedCallbacks delegate;
public DelegatingVerificationStateCallbacks(
#NonNull PhoneAuthProvider.OnVerificationStateChangedCallbacks delegate
) {
this.delegate = delegate;
}
#Override public void onCodeSent(
#NonNull String verificationId,
#NonNull PhoneAuthProvider.ForceResendingToken forceResendingToken
) {
if (delegate != null) delegate.onCodeSent(verificationId, forceResendingToken);
}
#Override public void onCodeAutoRetrievalTimeOut(#NonNull String s) {
if (delegate != null) delegate.onCodeAutoRetrievalTimeOut(s);
}
#Override public void onVerificationCompleted(#NonNull PhoneAuthCredential phoneAuthCredential) {
if (delegate != null) delegate.onVerificationCompleted(phoneAuthCredential);
}
#Override public void onVerificationFailed(#NonNull FirebaseException e) {
if (delegate != null) delegate.onVerificationFailed(e);
}
#Override public void close() {
delegate = null;
}
}
I've implemented Closeable for cleanup but you could implement RxJava's Disposable or whatever instead.
The usage pattern here is obvious and well-known:
public final class SomeScreen extends ActivityOrFragmentOrControllerOrWhatever {
private final ArrayList<Closeable> disposeBag = new ArrayList<>();
private void performAuth() {
DelegatingVerificationStateCallbacks callbacks =
new DelegatingVerificationStateCallbacks(
new OnVerificationStateChangedCallbacks() { … }
);
disposeBag.add(callbacks);
phoneAuthProvider.verifyPhoneNumber(…, callbacks);
}
#Override protected void onDestroy() {
for (Closeable c : disposeBag) {
try { c.close(); }
catch (IOException ignored) { }
}
disposeBag.clear();
}
}
Result: Firebase leaks a reference to empty and cheap DelegatingVerificationStateCallbacks, not to Activity.
Nulling references out yourself. You can take approach presented above to clear your own references to Activity. This implies these reference must be explicit, i. e. class must not be anonymous or inner to your activity. You must take full control of class constructor and fields, top-level class or nested static class is a good fit.
Weak reference. This is less explicit and involves some indirection but still works: you instantiate top-level or nested static class, pass Activity to constructor, wrap it in a WeakReference, and assign to a field. That's all, after some time WeakReference#get will start returning null.
Reflection. Very bad and unstable option which could help in some other situations. Sometimes your Activity could be leaked by Android SDK or vendor-specific code, and options from above don't apply. Then you can null out some private fields yourself. Don't do this for Firebase.

Related

Implementing Retry in pagination library android error - Null pointer Exception [duplicate]

This question already has answers here:
What is a NullPointerException, and how do I fix it?
(12 answers)
Closed 1 year ago.
I am trying to implement "Retry" mechanism while using pagination library android, in case of no internet or device went offline while loading items.
When I click the "retry" image I get error - "Attempt to invoke virtual method 'void com.myapp.myapp.repository.FeedDataSource.retryPagination()' on a null object reference"
FeedDataSource.java
public class FeedDataSource extends PageKeyedDataSource<Long, Feed> {
.....
//for retry
private LoadParams<Long> params;
private LoadCallback<Long, Feed> callback;
........
#Override
public void loadAfter(#NonNull LoadParams<Long> params, #NonNull LoadCallback<Long, Feed> callback) {
this.params = params;
this.callback = callback;
Log.e(TAG, "Loading rage" + params.key + " Count" + params.requestedLoadSize);
networkState.postValue(NetworkState.LOADING);
RestApi restApi = RetrofitApi.create();
Call<FeedResponse> call = restApi.fetchFeed(params.key, params.requestedLoadSize, username);
try
{
Response<FeedResponse> response = call.execute();
if(response.isSuccessful()){
FeedResponse feedResponse = response.body();
if(feedResponse !=null){
networkState.postValue(NetworkState.LOADED);
List<Feed> responseItems = feedResponse.getFeeds();
callback.onResult(responseItems, params.key + 1);
}
}
........
}
public void retryPagination()
{
loadAfter(params, callback);
}
}
FeedDataFactory.java
public class FeedDataFactory extends DataSource.Factory {
private MutableLiveData<FeedDataSource> mutableLiveData;
private FeedDataSource feedDataSource;
private AppController appController;
public FeedDataFactory(AppController appController){
this.appController = appController;
this.mutableLiveData = new MutableLiveData<FeedDataSource>();
}
#Override
public DataSource create() {
feedDataSource = new FeedDataSource(appController);
mutableLiveData.postValue(feedDataSource);
return feedDataSource;
}
public MutableLiveData<FeedDataSource> getMutableLiveData(){
return mutableLiveData;
}
}
FeedViewModel.java
public class FeedViewModel extends ViewModel {
..............
public void retry() {
FeedDataFactory feedDataFactory = new FeedDataFactory(appController);
feedDataFactory.getMutableLiveData().getValue().retryPagination();
}
FeedAdapter.java
public class FeedListAdapter extends PagedListAdapter<Feed, RecyclerView.ViewHolder> {
......
private final Callback callback;
public FeedListAdapter(#NonNull DiffUtil.ItemCallback<Feed> diffCallback, Callback callback, Context context) {
super(Feed.DIFF_CALLBACK);
this.context = context;
this.callback = callback;
}
public void bindView(NetworkState networkState) {
if (networkState != null && networkState.getStatus() == NetworkState.Status.FAILED) {
binding.refresh.setVisibility(View.VISIBLE);
binding.refresh.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
binding.refresh.setVisibility(View.GONE);
binding.progressBar.setVisibility(View.VISIBLE);
callback.onRetryClicked();
}
});
}
}
public interface Callback {
void onRetryClicked();
}
}
HomeFragment.java
public class HomeFragment extends Fragment implements ClickHandler, FeedListAdapter.Callback {
private FeedListAdapter feedListAdapter;
private FeedViewModel feedViewModel;
..............
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
feedViewModel = new ViewModelProvider(this).get(FeedViewModel.class);
feedViewModel.init(appController);
feedListAdapter = new FeedListAdapter(Feed.DIFF_CALLBACK, this, getContext());
recyclerView.setAdapter(feedListAdapter);
feedViewModel.getArticleLiveData().observe(getViewLifecycleOwner(), pagedList -> {
feedListAdapter.submitList(pagedList);
});
}
}
Please help, what am I doing wrong!
First of all, I want to thank you because your solution is a hint for me to implement the network retry thing, I really appreciate that.
Second, about the NullPointerException you're getting, it's because in your FeedViewModel.java, when the retry button is triggered, you create a whole new factory for datasource. It's not either necessary or right because the Factory wont create any new DataSource unless the previous one was invalidated. You should retrieve the DataSource through your current Factory.
Another problem I want to mention is that, if your request is failed, you don't call onResult(...) on the callback. Based on the documentation:
A callback can be called only once, and will throw if called again. It
is always valid for a DataSource loading method that takes a callback
to stash the callback and call it later. This enables DataSources to
be fully asynchronous, and to handle temporary, recoverable error
states (such as a network error that can be retried)
Here is my implementation in DataSource when onFailure() in Retrofit is called:
#Override
public void onFailure(Call<HotItemsResponse> call, Throwable t) {
loadingState.postValue(Contants.LoadingState.SUB_LOAD_ERROR);
//when the load is fail, dont call onResult() on the call back,
//just ignore it, update the loading state for the UI to handle reload
//callback.onResult(new ArrayList<ProductItem>(), currentPage);
}

Android asks for not to query on main thread, but asyc queries are delaying

As the title says, android needs queries out of main thread since it will trhow java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time otherwise. So I managed to make async queries as many tutorials explain, but it doesn't make so much sense (so far) as I could achieve.
public class NewDetalleDiarioActivity extends AppCompatActivity {
#Override
protected void onStart() {
super.onStart();
db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "database").build();
findPeriodo();
findDiario();
}
private void findPeriodo() {
periodo = Diarios.getPeriodo(db);
if (periodo == null) {
Intent intent = new Intent(NewDetalleDiarioActivity.this, NewPeriodoActivity.class);
startActivity(intent);
}
}
PROBLEM/ERROR:
If periodo is null, another activity is started, otherwise this one continues its thread.
The problem is that, when I debug it (which slows proceses, of course) periodo returns an instance from the database, but when I run the code without debugging, periodo is null.
public class Diarios {
public static Periodo getPeriodo(AppDatabase db) {
return Factory.getIntPeriodo().getPeriodo(db);
}
}
.
public class Factory {
private static IntPeriodo intPeriodo;
public static IntPeriodo getIntPeriodo() {
return (intPeriodo == null) ? intPeriodo = new BusPeriodo() : intPeriodo;
}
}
.
public class BusPeriodo implements IntPeriodo {
// I don't think it's necessary to post the interface...
#Override
public Periodo getPeriodo(final AppDatabase db) {
final Periodo[] periodo = new Periodo[1];
AsyncTask.execute(new Runnable() {
#Override
public void run() { //the async query that is driving me mad.
periodo[0] = db.periodoDao().getPeriodo(new Date());
}
});
return periodo[0];
}
}
What's the proper way to make select queries without getting them delayed?
The select query is indeed working, I don't think is necessary to post it (because it is returning an unique result when I debug), but it returns null when I run the code without debugging!! Please help.
SOLUTION:
As #user7041125 suggested, but instead I made a new class with an interface to call methods back to the activity, like this:
public class PeriodoBridge extends AsyncTask<Void, Void, Periodo> implements IntPeriodoBridge {
private WeakReference<Activity> weakActivity;
private IntPeriodoBridge caller; //implement this interface in the activity which needs to query
private AppDatabase db;
private Periodo periodo;
public PeriodoBridge(Activity activity, IntPeriodoBridge caller, AppDatabase db) {
weakActivity = new WeakReference<>(activity);
this.caller = caller; // assign activity instance to the local interface instance
this.db = db;
executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
#Override
protected Periodo doInBackground(Void... voids) {
periodo = Diarios.getPeriodo(db);
return periodo;
}
#Override
protected void onPostExecute(Periodo periodo) {
Activity activity = weakActivity.get();
if (activity == null) {
return;
}
if (periodo == null) {
Intent intent = new Intent(activity, NewPeriodoActivity.class);
activity.startActivity(intent);
} else {
setPeriodo(periodo);
}
}
#Override //this is an interface method (IntPeriodoBridge)
public void setPeriodo(Periodo periodo) {
caller.setPeriodo(periodo); //I can set the query result back to the activity class with this
}
Call the init method of this class. The activity implements IntPeriodoBridge and in that way I can set the query result object to the activity class.

Fix activity leak when listener keeps implicit reference

In the MessageFeedActivity onCreate method it load feeds by calling getMessageTypes method of CTFeedAPI class.
public class MessageFeedActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//Setting the listener
CTFeedAPI ctFeedAPI = new CTFeedAPI(new CTFeedAPI.CTFeedAPIListener() {
#Override
public void feedAPISuccessListener(Object object) {
// Handle Success
}
#Override
public void feedAPIErrorListener(int error) {
// Handle Error
}
});
ctFeedAPI.getMessageTypes();
}
#Override
protected void onDestroy() {
super.onDestroy();
}
}
and wait for CTFeedAPIListener response. And CTFeedAPI class make network request by calling performRequest method of NetworkRequest class as
public class CTFeedAPI implements NetworkListener {
private CTFeedAPIListener apiListener;
public CTFeedAPI(CTFeedAPIListener feedAPIListener) {
apiListener = feedAPIListener;
}
public void getMessageTypes() {
Map < String, String > params = new HashMap < > ();
params.put("f", "GetMessageTypes");
NetworkRequest networkRequest = new NetworkRequest(this);
networkRequest.performRequest();
}
public interface CTFeedAPIListener {
void feedAPISuccessListener(Object object);
void feedAPIErrorListener(int error);
}
}
and wait for NetworkListener response
public class NetworkRequest {
private NetworkListener mListener;
public interface NetworkListener {
void networkReqSuccessListener(String cacheKey, String tag, String response);
void networkReqErrorListener(String tag, int error);
}
public NetworkRequest(NetworkListener listener) {
this.mListener = listener;
}
public void performRequest(
// Perform Network Requests and respond as
if (mListener != null) {
if (success) {
mListener.networkReqSuccessListener(getUrl(), getTag(), response);
} else {
mListener.networkReqErrorListener(getTag(), err_msg);
}
}
}
When users press back key, before destroy the MessageFeedActivity, the system call 'onDestroy' method. And Unfortunately, because the background thread (performRequest method in NetworkRequest class) is still keep a reference to it, leak occurs.
So how to implement CTFeedAPIListener reference in MessageFeedActivity to remove leak.
In this design not only you will leak memory but also your code would be highly coupled and very hard to test; prone to bugs that are hard to detect. I would suggest you implement MVP or similar architecture. Your activity should never know anything about your network layer. Add a presenter layer that is responsible to request something on behalf of your activity and use interface to update your activity. Your presenter should access a business entity that is mapped from the response of repository layer, that is responsible for network or Db access and return values to the client presenter. This way your presenter and business logic layers would be decoupled and easy to test independently. In the future if business requirements change, your changes don't affect other layers. Please see this article for more information on the subject.
Weak reference objects, which do not prevent their referents from
being made finalizable, finalized, and then reclaimed. Weak references
are most often used to implement canonicalizing mappings.
Suppose that the garbage collector determines at a certain point in
time that an object is weakly reachable. At that time it will
atomically clear all weak references to that object and all weak
references to any other weakly-reachable objects from which that
object is reachable through a chain of strong and soft references. At
the same time it will declare all of the formerly weakly-reachable
objects to be finalizable. At the same time or at some later time it
will enqueue those newly-cleared weak references that are registered
with reference queues.
You can use Weak Reference:
import java.lang.ref.WeakReference;
public class NetworkRequest {
public interface NetworkListener {
void networkReqSuccessListener(String cacheKey, String tag, String response);
void networkReqErrorListener(String tag, int error);
}
private WeakReference<NetworkListener> mListener;
public NetworkRequest(NetworkListener listener) {
this.mListener = new WeakReference<NetworkListener>(listener);
}
public void performRequest(){
// Perform Network Requests and respond as
NetworkListener listener = mListener.get();
if (listener != null) {
if (success) listener.networkReqSuccessListener(getUrl(), getTag(), response);
else listener.networkReqErrorListener(getTag(), err_msg);
}
}
}
public class CTFeedAPI implements NetworkListener {
private WeakReference<CTFeedAPIListener> apiListener;
public CTFeedAPI(CTFeedAPIListener feedAPIListener) {
apiListener = new WeakReference<>(feedAPIListener);
}
public void getMessageTypes() {
Map < String, String > params = new HashMap < > ();
params.put("f", "GetMessageTypes");
NetworkRequest networkRequest = new NetworkRequest(this);
networkRequest.performRequest();
}
public interface CTFeedAPIListener {
void feedAPISuccessListener(Object object);
void feedAPIErrorListener(int error);
}
}
save CTFeedAPI and CTFeedAPIListener as instance variable of MessageFeedActivity to prevent GC collecting them when activity is presented:
public class MessageFeedActivity extends AppCompatActivity {
private CTFeedAPI ctFeedAPI = null;// keeping a reference to CTFeedAPI
private CTFeedAPIListener listener = null;// keeping a reference to listener
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//Setting the listener
listener = new CTFeedAPI.CTFeedAPIListener() {
#Override
public void feedAPISuccessListener(Object object) {
// Handle Success
}
#Override
public void feedAPIErrorListener(int error) {
// Handle Error
}
});
ctFeedAPI = new CTFeedAPI(listener);
ctFeedAPI.getMessageTypes();
}

Multiple Activities Implementing Same Listener

I know there are similar questions but it still doesn't answer my question in the manner I need for my current situation.
I have three activity presenters that each need to call a certain data remotely which will therefore call the activity presenter back when data arrives. To create this data listener I created an interface listener and since all three Presenters ask for the same data and retrieve it, all three presenters implement this interface listener.
Interface listener:
interface ListenerInterface {
onGotData();
}
Presenter one:
class PresenterOne implements ListenerInterface{
public void getData() {
DataManager dataManager = new DataManager();
dataManager.getData(this);
}
#Override
public void onGotData(Data data) {
//Do something with data
}
}
Presenter two very similar to presenter one:
class PresenterTwo implements ListenerInterface{
public void getData() {
DataManager dataManager = new DataManager();
dataManager.getData(this);
}
#Override
public void onGotData(Data data) {
//Do something with data
}
}
Assume Presenter three is exactly the same as the previous. The data manager class:
class DataManager {
public void getData(final ListenerInterface listener) {
//Gets data
addOnCompleteListener(new OnCompleteListener<Data data > () {
#Override
public void onComplete (#NonNull DataCall < Data > dataCall) {
listener.onGotData(dataCall.getResults());
}
});
}
}
Would doing so someone call all three presenters since the interface is the one doing the calling or only call the presenter that is passed? Is there anything I should worry about if I followed way? If anyone who knows the Android framework well could provide a detailed answer so I could learn from it more that would be great.
The reason I want to do this is I want to communicate through interfaces between classes.
Sorry if this question is simple for some people but I am still learning.
Thank you very much in advance.
you can use RxBus implementation to make global event (e.g. your onGotData).
First you have to create RxBus class.
public class RxBus {
private static RxBus instance;
private PublishSubject<Event> subject = PublishSubject.create();
public static synchronized RxBus getInstance() {
if (instance == null) {
instance = new RxBus();
}
return instance;
}
private RxBus(){}
public void postEvent(Event event){
subject.onNext(event);
}
public Observable<Event> getEvents(){
return subject;
}
}
And now, you should subscribe to it in BaseActivity or something like this (depends or your project structure).
private RxBus rxbus;
private Subscription rxBusSubscription;
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
rxBus = RxBus.getInstance();
}
#Override
protected void onResume() {
super.onResume();
if (shouldSubscribeRxBus()) {
rxBusSubscription = rxBus.getEvents()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(event -> {
if (event.getEventType() == Event.EventType.DATA_RECEIVED) {
onGotData(data);
}
});
}
}
Now implement you onGotData as you want.
When you catch data received call:
class DataManager {
public void getData(final ListenerInterface listener) {
//Gets data
addOnCompleteListener(new OnCompleteListener<Data data > () {
#Override
public void onComplete (#NonNull DataCall < Data > dataCall) {
RxBus.getInstance().postEvent(new GotDataEvent(dataCall.getResults()));
}
});
}
}
You can create your Event classes structure as you want.

RequestFactoryEditorDriver doesn't save full graph even though "with()" is called. Is circular reference an issue?

Could you guys please help me find where I made a mistake ?
I switched from SimpleBeanEditorDriver to RequestFactoryEditorDriver and my code no longer saves full graph even though with() method is called. But it correctly loads full graph in the constructor.
Could it be caused by circular reference between OrganizationProxy and PersonProxy ? I don't know what else to think :( It worked with SimpleBeanEditorDriver though.
Below is my client code. Let me know if you want me to add sources of proxies to this question (or you can see them here).
public class NewOrderView extends Composite
{
interface Binder extends UiBinder<Widget, NewOrderView> {}
private static Binder uiBinder = GWT.create(Binder.class);
interface Driver extends RequestFactoryEditorDriver<OrganizationProxy, OrganizationEditor> {}
Driver driver = GWT.create(Driver.class);
#UiField
Button save;
#UiField
OrganizationEditor orgEditor;
AdminRequestFactory requestFactory;
AdminRequestFactory.OrderRequestContext requestContext;
OrganizationProxy organization;
public NewOrderView()
{
initWidget(uiBinder.createAndBindUi(this));
requestFactory = createFactory();
requestContext = requestFactory.contextOrder();
driver.initialize(requestFactory, orgEditor);
String[] paths = driver.getPaths();
createFactory().contextOrder().findOrganizationById(1).with(paths).fire(new Receiver<OrganizationProxy>()
{
#Override
public void onSuccess(OrganizationProxy response)
{
if (response == null)
{
organization = requestContext.create(OrganizationProxy.class);
organization.setContactPerson(requestContext.create(PersonProxy.class));
} else
organization = requestContext.edit(response);
driver.edit(organization, requestContext);
}
#Override
public void onFailure(ServerFailure error)
{
createConfirmationDialogBox(error.getMessage()).center();
}
});
}
private static AdminRequestFactory createFactory()
{
AdminRequestFactory factory = GWT.create(AdminRequestFactory.class);
factory.initialize(new SimpleEventBus());
return factory;
}
#UiHandler("save")
void buttonClick(ClickEvent e)
{
e.stopPropagation();
save.setEnabled(false);
try
{
AdminRequestFactory.OrderRequestContext ctx = (AdminRequestFactory.OrderRequestContext) driver.flush();
if (!driver.hasErrors())
{
// Link to each other
PersonProxy contactPerson = organization.getContactPerson();
contactPerson.setOrganization(organization);
String[] paths = driver.getPaths();
ctx.saveOrganization(organization).with(paths).fire(new Receiver<Void>()
{
#Override
public void onSuccess(Void arg0)
{
createConfirmationDialogBox("Saved!").center();
}
#Override
public void onFailure(ServerFailure error)
{
createConfirmationDialogBox(error.getMessage()).center();
}
});
}
} finally
{
save.setEnabled(true);
}
}
}
with() is only used for retrieval of information, so your with() use with a void return type is useless (but harmless).
Whether a full graph is persisted is entirely up to your server-side code, which is intimately bound to your persistence API (JPA, JDO, etc.)
First, check that the Organization object you receive in your save() method on the server-side is correctly populated. If it's not the case, check your Locators (and/or static findXxx methods) ; otherwise, check your save() method's code.
Judging from the code above, I can't see a reason why it wouldn't work.
It took me some time to realize that the problem was the composite id of Person entity.
Below is the code snippet of PojoLocator that is used by my proxy entities.
public class PojoLocator extends Locator<DatastoreObject, Long>
{
#Override
public DatastoreObject find(Class<? extends DatastoreObject> clazz, Long id)
{
}
#Override
public Long getId(DatastoreObject domainObject)
{
}
}
In order to fetch child entity from DataStore you need to have id of a parent class. In order to achieve that I switched "ID class" for Locator<> to String which represents textual form of Objectify's Key<> class.
Here is how to looks now:
public class PojoLocator extends Locator<DatastoreObject, String>
{
#Override
public DatastoreObject find(Class<? extends DatastoreObject> clazz, String id)
{
Key<DatastoreObject> key = Key.create(id);
return ofy.load(key);
}
#Override
public String getId(DatastoreObject domainObject)
{
if (domainObject.getId() != null)
{
Key<DatastoreObject> key = ofy.fact().getKey(domainObject);
return key.getString();
} else
return null;
}
}
Please note that your implementation may slightly differ because I'm using Objectify4.

Categories

Resources