How to register StartActivityForResult event to ViewModel? [MVVM] - java

I have a problem implementing Google signin with MVVM in Java.
here, in a normal way you will see this sample code from Google:
PROBLEM:
in your activity:
#Override
public void onCreate(Bundle savedInstanceState) {
/* Here is the Issue:
* Google Object is defined in View - Activity
* I would like to have Google Object defined in my ViewModel
*/
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN).requestEmail().build();
mGoogleSignInClient = GoogleSignIn.getClient(this, gso);
}
// when Google Button CLicked
#Override
public void onClick(View v) { signIn(); }
private void signIn() {
/* Here is the Issue:
* I have to get this process done in View Model
* so view will not reference any Google Object
*/
Intent signInIntent = mGoogleSignInClient.getSignInIntent();
startActivityForResult(signInIntent, RC_SIGN_IN);
}
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// Below will be processed in ViewModel
GoogleSignInClient.getSignInIntent(...);
if (requestCode == RC_SIGN_IN) {
Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data);
handleSignInResult(task);
}
}
QUESTIONS: *see comment
so I came out with Idea Below:
in Activity:
// when Google Button CLicked
#Override
public void onClick(View v) { viewModel.loginGoogle(); }
private void subscribeUi() {
// register startActivityForResult Event to ViewModel and set this activity as receiver...
// viewModel.startActivityForResultEvent.setEventReceiver(this Activity)
// How to do this?
}
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
// send the result to View Model
viewModel.onResultFromActivity(requestCode,resultCode,data);
// escallate to super
super.onActivityResult(requestCode, resultCode, data)
}
now in ViewModel:
public void viewModelOnCreate() {
// This is what i want: Google object defined in View Model
// but I dont know how to call startActivityForResult from here?
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN).requestEmail().build();
mGoogleSignInClient = GoogleSignIn.getClient(getApplication(), gso);
}
// triggered when login button pressed
public void loginGoogle(){
// send Trigger startActivityForResult(getGoogleSignInIntent(), GOOGLE_SIGN_IN) this event should be catch later in my Activity
// How to do this?
// maybe something like:
// startActivityForResultEvent.sendEvent( ActivityNavigation.startActivityForResult startActivityForResult(getGoogleSignInIntent(), GOOGLE_SIGN_IN)
}
public void onResultFromActivity(int requestCode, int resultCode, Intent data){
// do whatever needed here after received result from Google
// for example:
if (requestCode == RC_SIGN_IN) {
Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data);
handleSignInResult(task);
}
}
any Idea how to get this achieved?
been scratching my head to get this done...
thanks and Appreciate the help :)
}

I think you can do the following, it is not hold global reference to context so it will not leak
public void loginGoogle(Context context){
if(isSigningIn)
return
context.startActivityForResult(getGoogleSignInIntent(), GOOGLE_SIGN_IN)
isSigningIn = true;
}

You can use SingleLiveData to open new screen. See:
https://proandroiddev.com/livedata-with-single-events-2395dea972a8
You create class with all necessary parameters to start activity
In ViewModel you create this class with parameters you need
You create single live data field in your ViewModel and observe it from activity/fragment
You send this class with SingleLiveData
create class:
public Enum Screen {
LOGIN
}
in ViewModel:
...
private SingleLiveData<Screen> onOpenScreen = new SingleLiveData<Screen>()
public SingleLiveData<Screen> observeScreenOpen() {
return onOpenScreen
}
public void loginGoogle(){
onOpenScreen.value = Screen.LOGIN
}
...
in activity/fragment
viewModel.observeScreenOpen(this, new Observer<Screen> {screen->
if(screen == Screen.LOGIN) {
//start your activity here
}
})

This seems to be disencourage by the documentation and function that replaced startActivityForResult:
Register a request to start an activity for result, designated by the given contract. This creates a record in the registry associated with this caller, managing request code, as well as conversions to/from Intent under the hood. This must be called unconditionally, as part of initialization path, typically as a field initializer of an Activity or Fragment.
If the host of this fragment is an ActivityResultRegistryOwner the ActivityResultRegistry of the host will be used. Otherwise, this will use the registry of the Fragment's Activity.
Attention for the "This must be called unconditionally, as part of initialization path".
Also notice this IlliegalStateException message:
Fragment [this] is attempting to registerForActivityResult after being created. Fragments must call "registerForActivityResult() before they are created (i.e. initialization, "onAttach(), or onCreate()).
So my suggestion is to put the contract and registerForActivityResult() on your Activity or Fragment onCreate and whatever you will be doing with the result in a function in your view model / domain class, which is basically what you are already doing.

What I would do is register a callback in the ViewModel that is invoked that the Activity can react to. Then the ViewModel can own the bulk of the business logic but does not have to have a reference to the Activity or Context and the Activity can deal with the Activity-specific stuff of launching an Intent.
Example:
Callback Interface:
interface OnSignInStartedListener {
void onSignInStarted(GoogleSignInClient client);
}
ViewModel:
public class ViewModel {
private final OnSignInStartedListener mListener;
public ViewModel(OnSignInStartedListener listener) {
mListener = listener;
}
public void viewModelOnCreate() {
// This is what i want: Google object defined in View Model
// but I dont know how to call startActivityForResult from here?
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN).requestEmail().build();
mGoogleSignInClient = GoogleSignIn.getClient(getApplication(), gso);
}
public void loginGoogle() {
// Invoke callback here to notify Activity
mListener.onSignInStarted(mGoogleSignInClient);
}
}
Activity:
protected void onCreate(Bundle savedInstanceState) {
...
mViewModel = new ViewModel(new OnSignInStartedListener() {
#Override
public void onSignInStarted(GoogleSignInClient client) {
startActivityForResult(client.getSignInIntent(), RC_SIGN_IN);
}
});
...
}
#Override
public void onClick(View v) {
// Invokes listener this activity created to start sign in flow
viewModel.loginGoogle();
}
Hope that helps!

Related

Exception while trying to retrieve data (a specific row) from Room database in android studio into a simple list rather than LiveData [duplicate]

In the main activity, I have LiveData which contains members and a click listener. If I click on a member, then his ID is passed with intent.putExtra. That ID is later passed on to the method open in this activity. With this activity, I want to see the details of a member. In my MemberInfo activity, I marked a line where my problem lies.
It shows me this error: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
My DAO consists this code:
#Query("SELECT * FROM member_table WHERE MemberID=:id")
Member getMemberInfo(long id);
This is my main activity:
public class MemberMainActivity extends AppCompatActivity implements MemberListAdapter.MemberClickListener{
private MemberViewModel mMemberViewModel;
private List<Member> mMember;
void setMember(List<Member> members) {
mMember = members;
}
public static final int NEW_MEMBER_ACTIVITY_REQUEST_CODE = 1;
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_member);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Intent intent = new Intent(MemberMainActivity.this, NewMemberActivity.class);
startActivityForResult(intent, NEW_MEMBER_ACTIVITY_REQUEST_CODE);
}
});
RecyclerView recyclerView = findViewById(R.id.recyclerviewcard_member);
final MemberListAdapter adapter = new MemberListAdapter(this);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
mMemberViewModel = ViewModelProviders.of(this).get(MemberViewModel.class);
mMemberViewModel.getAllMember().observe(this, new Observer<List<Member>>() {
#Override
public void onChanged(#Nullable final List<Member> members) {
mMember = members;
// Update the cached copy of the words in the adapter.
adapter.setMember(members);
}
});
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == NEW_MEMBER_ACTIVITY_REQUEST_CODE && resultCode == RESULT_OK) {
Member member = new Member(data.getStringExtra(NewMemberActivity.EXTRA_REPLY), data.getStringExtra(NewMemberActivity.EXTRA_REPLY2));
mMemberViewModel.insert(member);
} else {
Toast.makeText(
getApplicationContext(),
R.string.empty_not_saved,
Toast.LENGTH_LONG).show();
}
}
public void onMemberClick(int position) {
Member member = mMember.get(position);
Intent intent = new Intent(getApplicationContext(),MemberInfo.class);
intent.putExtra("MemberID", member.getId());
MemberInfo.open(this, member.getId());
}
}
This is my activity:
public class MemberInfo extends AppCompatActivity {
public static void open(Activity activity, long memberid) {
Intent intent = new Intent(activity, MemberInfo.class);
intent.putExtra("MemberID", memberid);
activity.startActivity(intent);
}
private List<Member> mMember;
private MemberViewModel mMemberViewModel;
void setMember(List<Member> members){
mMember = members;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memberinfo);
Log.i("okay", "memberinfo");
Intent intent = getIntent();
if (intent != null && intent.hasExtra("MemberID")) {
long memberid = intent.getLongExtra("MemberID", -1);
// TODO: get customer details based on customer id
TextView firstname = findViewById(R.id.layout_memberfirstname);
TextView surname = findViewById(R.id.layout_membersurname);
TextView balance = findViewById(R.id.layout_memberbalance);
-------------Member member = MemberRoomDatabase.getDatabase().memberDao().getMemberInfo(memberid);-------------
firstname.setText(member.getFirstname());
surname.setText(member.getSurname());
}
else {
Toast.makeText(
getApplicationContext(),
R.string.empty_not_saved,
Toast.LENGTH_LONG).show();
}
}
}
I thought that maybe it is because I'm missing a AsyncTask method. I tried this, but this also didn't work:
private static class insertMemberInfoAsyncTask extends AsyncTask<Member, Void, Void> {
private MemberDao mAsyncTaskDao;
insertMemberInfoAsyncTask(MemberDao dao) {
mAsyncTaskDao = dao;
}
#Override
protected Void doInBackground(Member... params) {
Member member = params[0];
mAsyncTaskDao.getMemberInfo(member.getId());
return null;
}
}
public Member getMemberInfo(long id) {
mAllMember = mMemberDao.getAllMember();
Member member = mMemberDao.getMemberInfo(id);
new insertMemberInfoAsyncTask(mMemberDao).execute(member);
return member;
}
I think I use the method wrong. Can anybody help me?
One option is to update your query to this:
#Query("SELECT * FROM member_table WHERE MemberID=:id")
LiveData<Member> getMemberInfo(long id);
(or similar, using Flowable). This avoids the need to manually create your own AsyncTask.
Returning the LiveData wrapper around the Member type automatically signals to Room that the query can/should be performed asynchronously. Per https://developer.android.com/training/data-storage/room/accessing-data (my emphasis):
Note: Room doesn't support database access on the main thread unless you've called allowMainThreadQueries() on the builder because it might lock the UI for a long period of time. Asynchronous queries—queries that return instances of LiveData or Flowable—are exempt from this rule because they asynchronously run the query on a background thread when needed.
You can use Future and Callable. So you would not be required to write a long asynctask and can perform your queries without adding allowMainThreadQueries() or using LiveData.
My dao query:-
#Query("SELECT * from user_data_table where SNO = 1")
UserData getDefaultData();
My repository method:-
public UserData getDefaultData() throws ExecutionException, InterruptedException {
Callable<UserData> callable = new Callable<UserData>() {
#Override
public UserData call() throws Exception {
return userDao.getDefaultData();
}
};
Future<UserData> future = Executors.newSingleThreadExecutor().submit(callable);
return future.get();
}
In my case, it works if you add Dispatcher.IO when you use coroutines:
viewModelScope.launch(Dispatchers.IO) {
//your database call
}
For me allowMainThreadQueries() works.
This allows room to support database access on the main thread.
See the following code
#Database(entities = [Word::class ],version = 1)
abstract class VocabularyDatabase:RoomDatabase() {
companion object {
private lateinit var INSTANCE:VocabularyDatabase
fun getInstance(context:Context):VocabularyDatabase= Room.databaseBuilder(
context,
VocabularyDatabase::class.java,
"vocabulary"
)
.createFromAsset("vocabulary.db")
.allowMainThreadQueries()
.build()
}
abstract fun dao():WordDao
}
Using Future and Callables can be an alternative here. By using Future and Callable you can get rid of AsyncTask and forcing your queries to the main thread.
The syntax would be as follow -
#Throws(ExecutionException::class, InterruptedException::class)
private fun canContinue(id: String): UserData{
val callable = Callable { userDao.getDefaultData() }
val future = Executors.newSingleThreadExecutor().submit(callable)
return future!!.get()
}
And, don't forget the null check for the data returned. Because it might be null

Activity to Fragment sending data and access arraylist in Fragment

I have a fragment which is present in Bottom Navigation Activity. The Fragments contain the custom recyclerview. There is a comment button when i press it opens another activity for comments. Below is in the RecyclerView adapter.
viewholder.commentlay.setOnClickListener(new View.OnClickListener()
{
#Override
public void onClick(View v)
{
//commenttofragment.clear();
Intent comment = new Intent(fp, com.fooddoof.fuddict.comment.class);
int id = dusers.get(position).getId();
int comcount = dusers.get(viewholder.getAdapterPosition()).getCommentcount();
comment.putExtra("id",id);
comment.putExtra("ownerid",userid);
comment.putExtra("maincommentposition",position);
comment.putExtra("commentcountonposition", comcount);
fp.startActivityForResult(comment,1);
}
});
In Comment activity after doing some tasks I need to send some values to this fragment. So I Override the OnBackPressed method. I have created a method in Fragment to receive it.
#Override
public void onBackPressed()
{
Bundle args = new Bundle();
args.putInt("maincommentcount",maincommentcount);
args.putInt("maincommentposition", maincommentposition);
FolowersPost f = new FolowersPost();
f.getdatafromcomment(args);
finish();
}
I receive it like below in Fragment.
public void getdatafromcomment(Bundle args)
{
int count = args.getInt("maincommentcount");
int p=args.getInt("maincommentposition",999999999);
Log.e("Shiva","count--->"+count+"p--->"+p);
}
The Values are received but I need to access the arraylist in Fragement which is passed in Adapter for displaying the recyclerView. But I am not able to access it while I am coming back to fragment which is present in the method under OnCreateView. I tried with OnResume to access it but works for some time only. I have declared the Arraylist as global variable also.
You are already using startActivityForResult. now you just need to use onActivityResult.
But you just need to start activity from fragment instead of from adapter.
onClick from fragment:
Intent comment = new Intent(getActivity(), com.fooddoof.fuddict.comment.class);
startActivityForResult(comment, 1);
onBackPressed in your comment activity:
#Override
public void onBackPressed() {
Intent returnIntent = new Intent();
returnIntent.putExtra("maincommentcount",10);
returnIntent.putExtra("maincommentposition",20);
setResult(Activity.RESULT_OK,returnIntent);
finish();
// super.onBackPressed();
}
onActivityResult in fragment:
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == 1) {
if (resultCode == Activity.RESULT_OK) {
int mMaincommentcount = data.getIntExtra("maincommentcount", 0);
int mMaincommentposition = data.getIntExtra("maincommentposition", 0);
System.out.println("mMaincommentcount = " + mMaincommentcount + ", mMaincommentposition = " + mMaincommentposition);
}
}
}

Go to an activity from anywhere

I am implementing a login system. The user needs to be redirected to the login activity from any previous activity if the token is no longer valid. I can go to the login activity with this
new Intent(CurrentActivity.this, NextActivity.class);
But this needs the current activity. I just want to go to the login activity no matter where I am. I cannot know where I am because this is inside an entirely different package.
you should probably register in the Application class to ActivityLifecycleCallbacks and if the user is not registered send them to the correct Activity.
just be sure to not endlessly send them from the login page to itself
EDIT:
adding some code and explanation.
In order to figure out if an Activity that shouldn't be alive is going through lifecycle events you'll need to implement some sort of a gate keeper. Previously it used to be some sort of static state that is kept in the Application class and holds the current activity and sometime even the stack of current activities.
This was far from a complete solution and had issues due to different tasks and even isolated procesies.
In API 14 Android introduced the Activity lifecycle callbacks which can be passed into the method registerActivityLifecycleCallbacks int the Application class.
What you want to do basically is the following:
class ThepooshApplication extends Application {
private static sIsRegistered = false;
public static setIsRegistered(boolean isRegistered) { sIsRegistered = isRegistered; }
public void onCreate() {
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks(){
#Override
void onActivityCreated(Activity activity, Bundle savedInstanceState){
if (!sIsRegistered && !(activity instanceof LoginActivity)) {
Intent loginIntent = new Intent(this, LoginActivity.class);
loginIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(loginIntent);
}
}
#Override
void onActivityStarted(Activity activity) { /*empty method*/ }
#Override
void onActivityResumed(Activity activity) { /*empty method*/ }
#Override
void onActivityPaused(Activity activity) { /*empty method*/ }
#Override
void onActivityStopped(Activity activity) { /*empty method*/ }
#Override
void onActivitySaveInstanceState(Activity activity, Bundle outState) { /*empty method*/ }
#Override
void onActivityDestroyed(Activity activity) { /*empty method*/ }
});
}
}
You must add FLAG_ACTIVITY_NEW_TASK flag to your intent
myIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Try some like this
Intent i = new Intent();
i.setClass(this,TestActivity.class);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
Replace TestActivity.class for your target activity

How to update Activity from thread started in another activity?

I have a Main activity and after click on button I start thread (but the thread is hidden in library and I have only callback in Main activity.
Now I want to start another activity (call A) where I want to put results from the thread.
Below is simplified code:
public class Main extends Activity {
XManager.ResultsCallback xResultsCallback = new XManager.ResultsCallback() {
// the method is called every 10 sec.
#Override
public void onResult(ArrayList<String> texts) {
}
};
XManager xManager = new xManager(xResultsCallback);
View.OnClickListener onClick = new View.OnClickListener() {
#Override
public void onClick(View arg0) {
XManager.start();
Intent i = new Intent(Main.this, A.class);
startActivity(i);
}
};
}
I want to update the content of A activity each time when onResult() method is called. How to do that?
Use LocalBroadcastManager,
In your Main Activity create function :
private void sendResult() {
Log.d("sender", "Broadcasting message");
Intent intent = new Intent("custom-event-name");
// You can also include some extra data.
intent.putExtra("message", "This is my result!");
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}
and add BroadcastReceiver in your A Activity
private BroadcastReceiver onResult= new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
Log.d("jazzy","onReceive called");
}
};
add on OnCreate
#Override
public void onCreate(Bundle savedInstanceState) {
// Register to receive messages.
LocalBroadcastManager.getInstance(this).registerReceiver(mMessageReceiver,
new IntentFilter("custom-event-name"));
}
add onDestroy
#Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mMessageReceiver);
super.onDestroy();
}
I have a suggestion that you should do as follows:
Start Your Activity A on button click
Inside Activity A declare your XManager instance with a callback present in A itself
Then start your XManager as XManager.start(); that way you would be getting all the callbacks in your desired activity.
Have a great day!
I think if you want to decouple the logic, beside you can use the Android BroadcastReceiver, the another flexible choice is to use the Bus
And you can integrate it with gradle easily
dependencies {
compile 'com.squareup:otto:+'
}

Serial scan using Zxing library - android

i'm developing a scan Barcode app on android , my application is simple and composed from an activity which contains a button and a textView which will receive the result of the scan.. The app works well but i want that i could realise serial scan in a raw. so after scanning a barcode i need that the capture Activity stay and the appli don't back to the button activity so i can scan the next Barcode. any solution please ?
this is my main java code :
public class MainActivity extends Activity {
private Button scan;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
scan= (Button)findViewById(R.id.btnScan);
scan.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// TODO Auto-generated method stub
Intent intent = new Intent("com.google.zxing.client.android.SCAN");
intent.putExtra("com.google.zxing.client.android.SCAN.SCAN_MODE", "QR_CODE_MODE");
startActivityForResult(intent, 0);
}
});
}
public boolean onTouchEvent(final MotionEvent event) {
IntentIntegrator integrator = new IntentIntegrator();
integrator.initiateScan(null);
return true;
}
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 0) {
if (resultCode == RESULT_OK) {
String contents = data.getStringExtra("SCAN_RESULT");
TextView tv = (TextView) findViewById(R.id.scanResult);
tv.setText(data.getStringExtra("SCAN_RESULT"));//this is the result
} else
if (resultCode == RESULT_CANCELED) {
// Handle cancel
} }
}
#Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
}
}
I had the same problem when I did my scanner activity, and the solution that I found was to make my mainActivity extends Zxing CaptureActivity, like this I overrided handleDecode and I avoided to switch between different activities (as you have to do to obtain your scanner result).
Anyhow, to restart the scanning process after a precedent scan I called the method
restartPreviewAfterDelay(0L)
(that is a method of CaptureActivity) in the onClick function of a button.
Take a look at that method, I think that it is what you need.
i finally found the solution i had just to add this code in the onActivityResult() declaration
Intent intent = new Intent("com.google.zxing.client.android.SCAN");
intent.putExtra("com.google.zxing.client.android.SCAN.SCAN_MODE", "QR_CODE_MODE");
startActivityForResult(intent, 0);
So after the scan is finished the app is ready to scan again instead of going back to the home activity

Categories

Resources