问题
I have this flow in my app:
declare endpoints:
public interface EndPoints {
@GET(Constants.POPULAR)
Call<AllData> getAllData(
@Query("api_key") String apiKey
);
}
Retrofit service:
private static EndPoints endPoints = retrofit.create(EndPoints.class);
public static EndPoints getEndpoints() {
return endPoints ;
}
And I call this inside my view model:
private void getDataFromApi() {
Call<AllData> call = RetrofitService.getEndPoints().getAllData(Constants.API_KEY);
call.enqueue(new Callback<AllData>() {
@Override
public void onResponse(Call<AllData> call, Response<AllData> response) {
}
if (response.isSuccessful()) {
_allData.setValue(response.body());
}
@Override
public void onFailure(Call<AllData> call, Throwable t) {
}
});
}
Base Activity:
public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(getLayoutId());
}
Progress bar layout(I updated this instead of using frame layout every xml file, I created xml called progress bar layout and I want to inflate this every call):
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/frame_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
</FrameLayout>
How can I set progress bar every api call?
Initally I checked in getDataFromApi
if is loading or not, set the value into bool LiveData
and observed this inside my activity.
The problem with this is it's an insane amount of code duplication.
Every api call I need to set loading state, every activity I need to observer the loading state and call View.Visible
and View.Hide
and every activity xml I need to create frame layout with progress bar.
I believe the answer is somewhere inside the base activity as it is the only place that can "control" all the activities in the app, but I can't think of a way of solving this problem
回答1:
Create a list to save all of your fragment and listen on the status:
List<FrameLayout> frameLayoutList = new ArrayList<>();
// register you own FrameLayout
frameLayoutList.add(frameLayout);
mainViewModel.getStaus().observe(this, new Observer<MainViewModel.Status>() {
@Override
public void onChanged(MainViewModel.Status status) {
if (status == MainViewModel.Status.LOADING) {
for (FrameLayout frameLayout : frameLayoutList) {
frameLayout.setVisibility(View.VISIBLE);
}
} else {
for (FrameLayout frameLayout : frameLayoutList) {
frameLayout.setVisibility(View.GONE);
}
frameLayout.setVisibility(View.GONE);
}
}
});
回答2:
If you used status
solely for purpose of showing ProgressBar
then you can try changing your getDataFromApi()
to take Context
object as argument and then show a AlertDialog
which contains a ProgressBar
(modify it as you like depending on your need) and then dismiss()
it in response.isSuccessful()
being true
.
private void getDataFromApi(Context context) {
// status.setValue(Status.LOADING);
// start showing progressbar instead of setting the status
// or actually u can do both depending on the usage of the `status` variable
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context);
ProgressBar progressBar = new ProgressBar(this);
progressBar.setIndeterminate(true);
builder.setView(progressBar);
final AlertDialog dialog = builder.show();
Call<AllData> call = RetrofitService.getEndPoints().getAllData(Constants.API_KEY);
call.enqueue(new Callback<AllData>() {
@Override
public void onResponse(Call<AllData> call, Response<AllData> response) {
}
if (response.isSuccessful()) {
//staus.setValue(Status.SUCCESS);
//dismiss the progress in success
dialog.dismiss();
_allData.setValue(response.body());
}
@Override
public void onFailure(Call<AllData> call, Throwable t) {
//staus.setValue(Status.ERROR);
}
});
}
回答3:
I just wrote this up. Works for me in my example and i hope this solution will work for you too or give you a better idea how to do it.
Step 1. add MyApplication class.
public class MyApplication extends Application {
private BaseActivity currentActivity;
@Override
public void onCreate() {
super.onCreate();
}
public BaseActivity getCurrentActivity() {
return currentActivity;
}
public void setCurrentActivity(BaseActivity currentActivity) {
this.currentActivity = currentActivity;
}
}
Step 2. In BaseActivity add method to save currentActivity in MyApplication class.
public void setCurrentActivity(BaseActivity activity) {
((MyApplication) getApplication()).setCurrentActivity(activity);
}
Step 3. Create ProgressBarHolder class - this will handle logic to add progress bar to activity layout.
add id to resource, it will be needed later to find progressbar reference when rotating the screen
<resources>
<item type="id" name="progress"/>
</resources>
now create ProgressBarHolder class
public class ProgressBarHolder {
private ProgressBar mProgressBar;
public ProgressBarHolder(Context context) {
mProgressBar = new ProgressBar(context, null, android.R.attr.progressBarStyleLarge);
mProgressBar.setId(R.id.progress);
mProgressBar.setIndeterminate(true);
mProgressBar.setVisibility(View.GONE);
}
//ADD VIEW LOGIC IS IN SHOW BECAUSE WHEN WE ROTATE THE SCREEN NEW LAYOUT WOULD BE CREATED AND WE WOULDN'T HAVE REFERENCE TO PROGRESSBAR
public void show(Context context) {
ProgressBar progressBar = ((Activity) context).findViewById(R.id.progress);
if (progressBar == null) {
if (mProgressBar.getParent() != null)
((ViewGroup) mProgressBar.getParent()).removeView(mProgressBar);
RelativeLayout.LayoutParams params = new
RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
RelativeLayout layout = new RelativeLayout(context);
layout.setGravity(Gravity.CENTER);
layout.addView(mProgressBar);
((Activity) context).addContentView(layout, params);
}
mProgressBar.setVisibility(View.VISIBLE);
}
public void hide() {
mProgressBar.setVisibility(View.GONE);
}
}
Step 4. Replace ViewModel with AndroidViewModel - this is important because AndroidViewModel has application reference
public class MyViewModel extends AndroidViewModel {
private ProgressBarHolder progressBarHolder;
public MutableLiveData<String> data = new MutableLiveData<>(); //NOT IMPORTANT, JUST TO OBSERVE RESULT IN MAINACTIVITY
public MyViewModel(@NonNull Application application) {
super(application);
progressBarHolder = new ProgressBarHolder(((MyApplication) application).getCurrentActivity());
}
// EXAMPLE - WAIT 5 SECONDS TO GET RESULT
public void getData() {
//TIME TO SHOW PROGRESS
progressBarHolder.show(((MyApplication) getApplication()).getCurrentActivity());
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
data.setValue(String.valueOf((int) (Math.random() * 50 + 1)));
//HIDE IT WHEN TASK IS FINISHED
progressBarHolder.hide();
}
}, 5000);
}
}
Step 5. Add MyViewModel to MainActivity and observe result.
public class MainActivity extends BaseActivity {
private MyViewModel myViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setCurrentActivity(this);
final TextView textView = findViewById(R.id.result);
Button hitMe = findViewById(R.id.hit_me);
ViewModelProvider.Factory factory = ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication());
myViewModel = new ViewModelProvider(this, factory).get(MyViewModel.class);
myViewModel.data.observe(this, new Observer<String>() {
@Override
public void onChanged(String result) {
textView.setText(result);
}
});
hitMe.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
myViewModel.getData();
}
});
}
}
回答4:
- I found a solution which will instead of a lot of duplicated code, will only require you to duplicate only 1 small method in Child Activities (1 line of code) & No XML duplications.
Solution
- Change BaseActivity as below (ignore naming I forgot and name)
ParentActivity.java
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.LiveData;
import android.os.Bundle;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ProgressBar;
import com.maproductions.mohamedalaa.stackoverflow_solutions.R;
/**
* Created by <a href="https://github.com/MohamedAlaaEldin636">Mohamed</a> on 6/4/2020.
*/
public abstract class ParentActivity extends AppCompatActivity {
/**
* I didn't see you using data binding that's why this code doesn't have it,
* but it's highly recommended
*/
@Override
final protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_parent);
FrameLayout frameLayout = findViewById(R.id.rootFrameLayout);
ProgressBar progressBar = findViewById(R.id.progressBar);
// Get reference of the child class root view, and add it if wasn't already added
View rootView;
if (frameLayout.getChildCount() == 1) {
rootView = getLayoutInflater().inflate(getLayoutResource(), frameLayout, false);
frameLayout.addView(
rootView,
0,
new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
);
}else {
rootView = frameLayout.getChildAt(0);
}
// Initial state
if (progressBar.getVisibility() == View.VISIBLE) {
rootView.setVisibility(View.GONE);
}
// Child class onCreate code
customOnCreate(rootView, savedInstanceState);
// Observe data changes
getIsLoadingLiveData().observe(this, isLoading -> {
if (isLoading == null || isLoading) {
progressBar.setVisibility(View.VISIBLE);
rootView.setVisibility(View.GONE);
}else {
progressBar.setVisibility(View.GONE);
rootView.setVisibility(View.VISIBLE);
}
});
}
/** Place here the layout resource that you would put in {@link #setContentView(View)} */
@LayoutRes
protected abstract int getLayoutResource();
/**
* Place here the code that you would place in {@link #onCreate(Bundle)},
* And DO NOT CALL {@link #setContentView(View)} it will be auto handled for you
* <br/>
* Also Note this is called before calling {@link #getIsLoadingLiveData()} in case you are
* initialization fields here that are needed to be accessed there.
*/
protected abstract void customOnCreate(@NonNull View rootView, @Nullable Bundle savedInstanceState);
/**
* return a live data value indicating isLoading
*/
@NonNull
protected abstract LiveData<Boolean> getIsLoadingLiveData();
}
@layout/activity_parent.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rootFrameLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.ParentActivity">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
ChildActivity.java
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProviders;
import android.os.Bundle;
import android.view.View;
import com.google.android.material.button.MaterialButton;
import com.maproductions.mohamedalaa.stackoverflow_solutions.R;
import com.maproductions.mohamedalaa.stackoverflow_solutions.view_model.ChildActivityViewModel;
/**
* Created by <a href="https://github.com/MohamedAlaaEldin636">Mohamed</a> on 6/4/2020.
*/
public class ChildActivity extends ParentActivity {
private ChildActivityViewModel viewModel;
@Override
protected int getLayoutResource() {
// Note won't need to add progress bar in this or any other layout.
return R.layout.activity_child;
}
@Override
protected void customOnCreate(@NonNull View rootView, @Nullable Bundle savedInstanceState) {
// Initialize view model
viewModel = ViewModelProviders.of(this).get(ChildActivityViewModel.class);
// Start loading data
viewModel.startLoadingDataFromApi();
// Get references of views and set them up here.
MaterialButton materialButton = rootView.findViewById(R.id.materialButton);
materialButton.setOnClickListener(null);
// Other Views ...
}
// The only duplicated code, but is a must.
@NonNull
@Override
protected LiveData<Boolean> getIsLoadingLiveData() {
return androidx.lifecycle.Transformations.map(viewModel.dataFromApi, input ->
input == null
);
}
}
ChildActivityViewModel.java
import android.os.Handler;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.maproductions.mohamedalaa.stackoverflow_solutions.models.DataFromApi;
import com.maproductions.mohamedalaa.stackoverflow_solutions.models.FakeDataFromApi;
/**
* Created by <a href="https://github.com/MohamedAlaaEldin636">Mohamed</a> on 6/4/2020.
*/
public class ChildActivityViewModel extends ViewModel {
public MutableLiveData<DataFromApi> dataFromApi = new MutableLiveData<>();
public void startLoadingDataFromApi() {
// Mock the api loading time
try {
new Handler().postDelayed(() -> {
// Do your magic here then change only your data value and isLoading will be auto changed,
// thanks to Transformations.map()
dataFromApi.setValue(FakeDataFromApi.get());
}, 5_000);
}catch (Throwable throwable) {
// Do nothing.
}
}
}
来源:https://stackoverflow.com/questions/62066275/how-to-inflate-progressbar-layout-without-code-duplication-when-there-is-an-api