Как раздуть макет progressBar без дублирования кода при вызове api - PullRequest
7 голосов
/ 28 мая 2020

В моем приложении есть этот поток:

объявить конечные точки:

public interface EndPoints {
    @GET(Constants.POPULAR)
    Call<AllData> getAllData(
            @Query("api_key") String apiKey
    );
}

Услуга модернизации:

  private static EndPoints endPoints = retrofit.create(EndPoints.class);

    public static EndPoints getEndpoints() {
        return  endPoints ;
    }

И я вызываю это внутри своей модели представления:

   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) {
        }
    });

}

Базовая активность:

public abstract class BaseActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getLayoutId());
    }

Макет индикатора выполнения (я обновил его вместо использования макета кадра в каждом xml файле, я создал xml, называемый макетом индикатора выполнения, и я хочу раздувать это каждый вызов):

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

Как я могу установить индикатор выполнения для каждого вызова API?

Изначально я проверил getDataFromApi, загружается или нет, установил значение в bool LiveData и заметил это внутри своей активности.

Проблема в том, что это безумное количество дублирование кода.

Каждый вызов API, который мне нужен для установки состояния загрузки, каждое действие, которое мне нужно для наблюдения за состоянием загрузки и вызовы View.Visible и View.Hide, и каждое действие xml, которое мне нужно для создания макета кадра с индикатор выполнения.

Я считаю, что ответ находится где-то внутри базового действия, поскольку это единственное место, которое может «контролировать» все действия в приложении, но я не могу придумать способ решения этой проблемы

Ответы [ 4 ]

0 голосов
/ 05 июня 2020
  • Я нашел решение, которое вместо большого количества дублированного кода потребует от вас дублировать только 1 небольшой метод в дочерних действиях (1 строка кода) и не XML дублирований.

Решение

  1. Измените BaseActivity, как показано ниже (игнорируйте имена, которые я забыл, и имя)

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

}
0 голосов
/ 02 июня 2020

Если вы использовали status исключительно для показа ProgressBar, вы можете попробовать изменить свой getDataFromApi(), чтобы он принимал Context объект в качестве аргумента, а затем показать AlertDialog, который содержит ProgressBar (измените его как вам нравится, в зависимости от ваших потребностей), а затем dismiss() он в response.isSuccessful() будет 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);
            }
        });

    }
0 голосов
/ 04 июня 2020

Я только что написал это. В моем примере работает у меня, и я надеюсь, что это решение подойдет и вам, или даст вам лучшее представление о том, как это сделать.

Шаг 1. Добавьте класс MyApplication.

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. В BaseActivity добавьте метод для сохранения currentActivity в классе MyApplication.

public void setCurrentActivity(BaseActivity activity) {
    ((MyApplication) getApplication()).setCurrentActivity(activity);
}

Шаг 3. Создайте класс ProgressBarHolder - он будет обрабатывать logi c для добавления индикатора выполнения в макет активности.

добавьте идентификатор к ресурсу, он понадобится позже, чтобы найти ссылку на индикатор выполнения при повороте экрана

<resources>
    <item type="id" name="progress"/>
</resources>

теперь создайте класс ProgressBarHolder

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);
    }
} 

Шаг 4. Замените ViewModel на AndroidViewModel - это важно, потому что AndroidViewModel имеет ссылку на приложение

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);
    }
}

Шаг 5. Добавьте MyViewModel в MainActivity и наблюдайте за результатом.

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();
            }
        });
    }
}
0 голосов
/ 29 мая 2020

Создайте список, чтобы сохранить весь ваш фрагмент и прослушать статус:

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);
        }
    }
});
...