How to inflate progressBar layout without code duplication when there is an api call

夙愿已清 提交于 2020-06-25 02:07:10

问题


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

  1. 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!