简体   繁体   中英

Android MVVM architecture and observing changes on data from an API

I'm new to the Android MVVM architecture. I have an API running locally with data ("deals") in it. I'd like to simply make a request to the API and display that data in a text field. Currently the data does not show up when the fragment is first loaded, but if I go to another activity and then back to the fragment it loads.

There are 3 classes of importance here.

DashboardViewModel.java:

package com.example.android_client.ui.dashboard;

import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;

import com.example.android_client.models.Deal;
import com.example.android_client.repository.Repository;

import java.util.List;

public class DashboardViewModel extends ViewModel {

    private MutableLiveData<String> mText;
    private Repository repository;
    private MutableLiveData<List<Deal>> deals = null;

    public void init() {
        if(this.deals == null) {
            this.repository = Repository.getInstance();
            this.deals = this.repository.getDeals();
        }
    }

    public DashboardViewModel() {
        this.mText = new MutableLiveData<>();
    }

    public LiveData<List<Deal>> getDeals() {
        return this.deals;
    }
}

DashboardFragment.java:

package com.example.android_client.ui.dashboard;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;

import com.example.android_client.R;
import com.example.android_client.models.Deal;

import java.util.List;

public class DashboardFragment extends Fragment {

    private DashboardViewModel dashboardViewModel;

    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.fragment_dashboard, container, false);
        final TextView textView = root.findViewById(R.id.text_dashboard);
        dashboardViewModel = ViewModelProviders.of(this).get(DashboardViewModel.class);
        dashboardViewModel.init();
        dashboardViewModel.getDeals().observe(this, new Observer<List<Deal>>() {
            @Override
            public void onChanged(List<Deal> deals) {
                if (deals != null && !deals.isEmpty()) {
                    System.out.println(deals.get(0).toString());
                    textView.setText(deals.get(0).toString());
                }
            }
        });
        return root;
    }
}

and Repository.java:

package com.example.android_client.repository;

import androidx.lifecycle.MutableLiveData;

import com.example.android_client.models.Deal;
import com.google.gson.Gson;

import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

public class Repository {

    private static Repository instance;
    private ArrayList<Deal> dealsList = new ArrayList<>();
    private final OkHttpClient client = new OkHttpClient();

    public static Repository getInstance() {
        if(instance == null) {
            instance = new Repository();
        }
        return instance;
    }

    private Repository() {}

    public MutableLiveData<List<Deal>> getDeals() {
        setDeals();
        MutableLiveData<List<Deal>> deals = new MutableLiveData<>();
        deals.setValue(dealsList);
        return deals;
    }

    private void setDeals() {
        Request request = new Request.Builder()
                .url("http://10.0.2.2:8000/api/deals?<params here>")
                .build();

        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                e.printStackTrace();
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                try (ResponseBody responseBody = response.body()) {
                    if (!response.isSuccessful()) {
                        throw new IOException("Unexpected code " + response);
                    }
                    String jsonDeals = responseBody.string(); // can only call string() once or you'll get an IllegalStateException
                    Deal[] deals = new Gson().fromJson(jsonDeals, Deal[].class);
                    dealsList = new ArrayList<>(Arrays.asList(deals));
                }
            }
        });

    }
}

When stepping through the code in the Repository class I can see that setDeals() is called when I load the fragment, and the request in the callback is queued. The first time getDeals() returns, it returns a list of 0 deals (within the MutableLiveData object).

onResponse in the callback doesn't run until the fragment is already loaded. When debugging I can see that the data is in the objects (all the Gson stuff works fine), but onChanged doesn't get called again (which sets the text view).

Am I not observing changes on the deals properly?

I think this would help. Try postValue on MutableLiveData in onResponse of network call. Please change your repository class like below:

package com.example.android_client.repository;
import androidx.lifecycle.MutableLiveData;

import com.example.android_client.models.Deal;
import com.google.gson.Gson;

import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

public class Repository {

private static Repository instance;
private ArrayList<Deal> dealsList = new ArrayList<>();
private final OkHttpClient client = new OkHttpClient();
MutableLiveData<List<Deal>> deals = new MutableLiveData<>();

public static Repository getInstance() {
    if(instance == null) {
        instance = new Repository();
    }
    return instance;
}

private Repository() {}

private MutableLiveData<List<Deal>> getDeals() {
    Request request = new Request.Builder()
            .url("http://10.0.2.2:8000/api/deals?<params here>")
            .build();

    client.newCall(request).enqueue(new Callback() {
        @Override
        public void onFailure(@NotNull Call call, @NotNull IOException e) {
            e.printStackTrace();
        }

        @Override
        public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
            try (ResponseBody responseBody = response.body()) {
                if (!response.isSuccessful()) {
                    throw new IOException("Unexpected code " + response);
                }
                String jsonDeals = responseBody.string(); // can only call string() once or you'll get an IllegalStateException
                Deal[] deals = new Gson().fromJson(jsonDeals, Deal[].class);
                dealsList = new ArrayList<>(Arrays.asList(deals));
                deals.postValue(dealsList);
            }
        }
    });
 return deals;
}
}

Your code is not working due to a new live data instance be created whenever getDeals() is called and the api response value be informed to other live data instance. You must set api response value to same instance of MutableLiveData returned by getDeals()

I'm not saying that it is the best architectural solution, but if you create a mutable live data as a class attribute and return it whenever getDeals() is called. Probably, it's going to work.

Also, a good practice is return a LiveData and not a MutableLiveData to not allowing a external component modify the internal value.

Please, take a look at the piece of code below.

OBS: Maybe, there is some syntax error, because I have not compiled it

    import com.example.android_client.models.Deal;
    import com.google.gson.Gson;
    import org.jetbrains.annotations.NotNull;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;

    import okhttp3.Call;
    import okhttp3.Callback;
    import okhttp3.OkHttpClient;
    import okhttp3.Request;
    import okhttp3.Response;
    import okhttp3.ResponseBody;

        public class Repository {

        private static Repository instance;
        private ArrayList<Deal> dealsList = new ArrayList<>();
        private final OkHttpClient client = new OkHttpClient();
        private MutableLiveData<List<Deal>> _deals = new MutableLiveData<>();                         
        private LiveData<List<Deal>> deals = _deals


        public static Repository getInstance() {
            if(instance == null) {
                instance = new Repository();
            }
            return instance;
        }

        private Repository() {}

        public LiveData<List<Deal>> getDeals() {
            setDeals();
            return deals;
        }

        private void setDeals() {
            Request request = new Request.Builder()
                    .url("http://10.0.2.2:8000/api/deals?<params here>")
                    .build();

            client.newCall(request).enqueue(new Callback() {
                @Override
                public void onFailure(@NotNull Call call, @NotNull IOException e) {
                    e.printStackTrace();
                }

                @Override
                public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                    try (ResponseBody responseBody = response.body()) {
                        if (!response.isSuccessful()) {
                            throw new IOException("Unexpected code " + response);
                        }
                        String jsonDeals = responseBody.string(); // can only call string() once or you'll get an IllegalStateException
                        Deal[] deals = new Gson().fromJson(jsonDeals, Deal[].class);
                        dealsList = new ArrayList<>(Arrays.asList(deals));
                        _deals.setValue(dealsList);

                    }
                }
            });

        }

}
When 

in your repository class in function get deals. you are initializing live data. requesting url in background thread and posting value on live data which is not received from server yet.

to solve this create livedata instance in constructor of repository and postvalue on livedata in onResponse callback.

//sorry for bad writting, posted from mobile.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM