Comprehensive implementation of Android OkHttp + Glide + RecyclerView + ButterKnife popular framework

requirement

Use one OkHttp to obtain muke.com movie data (20 pieces), use Glide to load pictures and display them in each movie Item, and search and display them by movie name and type.

Operation effect

Task description

1, Contents displayed on the home page:

  • Movie search area (name, spinner list, search button)
  • Movie display area (RecyclerView 2 column display)

2, Movie details

  • Movie details (UI layout)

3, Implementation of search function

  • Movie name search
  • Movie type search
  • Overlay search (name + type)

Note: access address of movie data: www.imooc.com com/api/movie
The name and type can be accessed by adding parameters after the address, such as:
www.imooc.com/api/movie?title = Galaxy guard 2 & types = action

Frame address

OkHttp
Glide
ButterKnife

analysis

Movie: store the data of each movie
INetCallBack: network request callback interface
MovieOkHttpUtils: use OkHttp to provide a way to request movie data from the network
Movierrecyclerviewadapter: implements the interface layout and provides methods to update the UI
MovieBiz: obtain the returned results from the network, provide the method to analyze the movie data, and realize the UI layout

MainActivity: application main interface
MovieDetailsActivity: movie information details page

code implementation

Involving network request operation and the use of various frameworks, it is necessary to complete network configuration and load relevant dependencies according to the framework address

Movie

This is relatively simple. It declares the relevant properties of the movie, the full parameter construction method and some get methods. For convenience, most of the parameters are set to String type, and the Serializable interface is implemented to prepare for the transfer of parameters between different activities

public class Movie implements Serializable {
    private String _id, average, title, description, directorsName, year, types, imageUrl, castsName;
    private int stars;

    public Movie(String _id, String average, int stars, String title, String description, String directorsName, String year, String types, String castsName, String imageUrl) {
        this._id = _id;
        this.average = average;
        this.stars = stars;
        this.title = title;
        this.description = description;
        this.directorsName = directorsName;
        this.year = year;
        this.types = types;
        this.castsName = castsName;
        this.imageUrl = imageUrl;
    }
	
	// Getter and Setter
}

INetCallBack

What is declared here is an interface, which declares two methods for callback in network request

public interface INetCallBack {
    void onSuccess(String response);
    void onFailed(Throwable ex);
}

MovieOkHttpUtils

This class needs to provide a doGet() method to obtain network data by implementing OkHttp, and complete the callback of INetCallBack interface in this method

	// OkHttp network request method, return result string
    public void doGet(String url, INetCallBack callBack){
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url(url)
                .build();
        Call call = client.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                mUiHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        callBack.onFailed(e);
                    }
                });
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                String respStr = null;
                try {
                    respStr = response.body().string();
                } catch (IOException e) {
                    mUiHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            callBack.onFailed(e);
                        }
                    });
                    return;
                }
                String finalRespStr = respStr;
                mUiHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        callBack.onSuccess(finalRespStr);
                    }
                });
            }
        });
    }

This class can be designed in singleton mode. In actual development, only one object providing doGet() method is needed

MovieRecyclerViewAdapter

Movie information display is realized through RecyclerView. Here, grid layout and linear layout need to be realized

The implementation is divided into the following four steps:

  1. Create a class that inherits recyclerview Adapter
  2. Create an internal class binding ViewHolder (inherit RecyclerView.ViewHolder and complete control initialization in the class)
  3. Related methods of implementing Adapter
  4. Set sub item click listen

Because the onBindViewHolder() method in this class runs on the UI thread, changes to the UI are made directly in this class
Here you need to use Glide to complete the acquisition and loading of network pictures

public class MovieRecyclerViewAdapter extends RecyclerView.Adapter<MovieRecyclerViewAdapter.ViewHolder> {

    private Context context;
    private List<Movie> data;
    private OnItemClickListener onItemClickListener;
    private RecyclerView recyclerView;

    public MovieRecyclerViewAdapter(Context context, RecyclerView recyclerView) {
        this.context = context;
        this.data = new ArrayList<>();
        this.recyclerView = recyclerView;
    }

    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    public void setData(List<Movie> data) {
        this.data = data;
        notifyDataSetChanged();
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        if (recyclerView.getLayoutManager().getClass() == GridLayoutManager.class) {
            return new ViewHolder(LayoutInflater.from(context).inflate(R.layout.itemview_movie_grid, parent, false));
        } else if (recyclerView.getLayoutManager().getClass() == LinearLayoutManager.class) {
            return new ViewHolder(LayoutInflater.from(context).inflate(R.layout.itemview_movie_linear, parent, false));
        }
        return null;
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        holder.name.setText(data.get(position).getTitle());
        holder.stars.setRating((float) data.get(position).getStars() / 10);
        holder.average.setText(data.get(position).getAverage());
        holder.directorsName.setText(data.get(position).getDirectorsName());
        holder.castName.setText(data.get(position).getCastsName());
        holder.year.setText(data.get(position).getYear());

        // Download the picture through Glide and display it
        Glide.with(context)
                .load(data.get(position).getImageUrl())
                .into(holder.image);

        // Set sub item click event
        holder.view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (onItemClickListener != null) {
                    onItemClickListener.onItemClick(position);
                }
            }
        });
    }

    @Override
    public int getItemCount() {
        return data.size();
    }

    // Sub item click callback interface
    public interface OnItemClickListener {
        void onItemClick(int position);
    }
    
    class ViewHolder extends RecyclerView.ViewHolder {
        @BindView(R.id.image)
        ImageView image;
        @BindView(R.id.name)
        TextView name;
        @BindView(R.id.ratingbar)
        RatingBar stars;
        @BindView(R.id.average)
        TextView average;
        @BindView(R.id.directorsname)
        TextView directorsName;
        @BindView(R.id.castsname)
        TextView castName;
        @BindView(R.id.year)
        TextView year;

        View view;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
            view = itemView;
        }
    }
}

Although there are fewer Item controls in grid layout than in linear layout, you can add the same controls to both layout files, set the same id, and then hide some controls as needed
In this way, you only need to determine which layout file to use by judging the layout of the incoming recyclerView object, and there is no need to deliberately distinguish different layouts when injecting the View with ButterKnife

MovieBiz

Realize the method of obtaining and analyzing network data, realize the method of UI layout, and also realize the sub item click event of RecyclerView
Here, a Boolean parameter isLoadAll is passed in, which is used to distinguish between grid layout and linear layout

public class MovieBiz {

    private Context context;
    private List<Movie> movieList;
    private RecyclerView recyclerView;
    private MovieRecyclerViewAdapter adapter;

    public MovieBiz(Context context, RecyclerView recyclerView) {
        this.context = context;
        this.recyclerView = recyclerView;
    }

    // Get data from the network
    // Parameter 1: link address string parameter 2: get all data
    public void getMovie(String url, Boolean isLoadAll) {
        movieList = new ArrayList<>();
        MovieOkHttpUtils.getInstance()
                .doGet(url, new INetCallBack() {
                    @Override
                    public void onSuccess(String response) {
                        // Analytical results
                        try {
                            JSONObject root = new JSONObject(response);
                            int total = root.optInt("total");
                            JSONArray rootArray = root.optJSONArray("movies");

                            for (int i = 0; i < total; i++) {
                                movieList.add(parseResponse(rootArray.optJSONObject(i)));
                            }
                        } catch (JSONException e) {
                            e.printStackTrace();
                        }

                        if(isLoadAll){
                            MainActivity.movies = movieList;
                        }
                        setUiRecyclerView(movieList,isLoadAll);
                    }

                    @Override
                    public void onFailed(Throwable ex) {
                        Toast.makeText(context, "Network error", Toast.LENGTH_SHORT).show();
                    }
                });
    }

    // Set UI interface, RecyclerView grid layout
    public void setUiRecyclerView(List<Movie> movieList, Boolean isLoadAll) {
        if (isLoadAll) {
            GridLayoutManager gridLayoutManager = new GridLayoutManager(context, 2);
            recyclerView.setLayoutManager(gridLayoutManager);
        }else{
            LinearLayoutManager linearLayoutManager = new LinearLayoutManager(context);
            recyclerView.setLayoutManager(linearLayoutManager);
        }
        adapter = new MovieRecyclerViewAdapter(context,recyclerView);
        adapter.setData(movieList);
        recyclerView.setAdapter(adapter);

        // Sub item click event listening
        adapter.setOnItemClickListener(new MovieRecyclerViewAdapter.OnItemClickListener() {
            @Override
            public void onItemClick(int position) {
                Movie movie = movieList.get(position);
                Intent it = new Intent(context, MovieDetailsActivity.class);
                it.putExtra("movie",movie);
                context.startActivity(it);
            }
        });
    }

    // Parse the returned string
    public Movie parseResponse(JSONObject root) {
        if (root != null) {
            String id = root.optString("id");
            String average = new Rating(root).getAverage();
            int stars = new Rating(root).getStars();
            String types = getTypes(root.optJSONArray("types"));
            String title = root.optString("title");
            String description = root.optString("description");
            String castsName = getCasts(root.optJSONArray("casts"));
            String directorsName = root.optJSONArray("directors").optJSONObject(0).optString("name");
            String year = root.optString("year");
            String imageUrl = root.optString("imageUrl");

            // Create Movie object
            Movie movie = new Movie(id, average, stars, title, description, directorsName, year, types, castsName, imageUrl);
            return movie;
        }
        return null;
    }

    // Provide data string
    private static String getCasts(JSONArray casts) {
        int len = casts.length();
        String str = new String();
        for (int i = 0; i < len; i++) {
            JSONObject cast = casts.optJSONObject(i);
            str += cast.optString("name");
            if (i < len-1){
                str += " ";
            }
        }
        return str;
    }

    // Get type string
    private static String getTypes(JSONArray types) {
        int len = types.length();
        String str = new String();
        for (int i = 0; i < len; i++) {
            str += types.optString(i);
            if (i != len - 1) {
                str += "/";
            }
        }
        return str;
    }

    // Provide star data
    static class Rating {
        private String average;
        private int stars;

        public Rating(JSONObject root) {
            JSONObject rating = root.optJSONObject("rating");
            average = rating.optString("average");
            stars = rating.optInt("stars");
        }

        public String getAverage() {
            return average;
        }

        public int getStars() {
            return stars;
        }
    }
}

MainActivity

First try ButterKnife to inject View

    @BindView(R.id.toolbar)
    Toolbar toolbar;
    @BindView(R.id.fab)
    FloatingActionButton fab;
    @BindView(R.id.moviename_edit)
    EditText movieName;
    @BindView(R.id.movietypes_spinner)
    Spinner types_spinner;
    @BindView(R.id.search_img)
    ImageView search;
    @BindView(R.id.index_recyclerview)
    RecyclerView recyclerView;
    @BindView(R.id.tips_txt)
    TextView tips;
    @BindView(R.id.back_txt)
    TextView back;
    @BindArray(R.array.movieclass)
    String[] items;
    private ArrayAdapter<String> arrayAdapter_spinner;
    private MovieBiz movieBiz;
    private String type;
    private List<Movie> searchList;
    public static List<Movie> movies = new ArrayList<>();

Initialize the home page according to the method provided

    private void initView() {
        setSupportActionBar(toolbar);
        setTitle("Muke film");

        // Initialize home page display
        movieBiz = new MovieBiz(this, recyclerView);
        movieBiz.getMovie("http://www.imooc.com/api/movie",true);
        
        // Initialize Spinner
        arrayAdapter_spinner = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, items);
        types_spinner.setAdapter(arrayAdapter_spinner);
    }

Initializes the click event of the control
Note: ButterKnife cannot inject the child click event of Spinner

    private void initEvent() {
        // Spinner sub item click event
        types_spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                type = items[position];
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {

            }
        });
    }

Click events are implemented through ButterKnife

    // Search for picture click events
    @OnClick(R.id.search_img)
    public void searchOnClick() {
        searchList = new ArrayList<>();
        tips.setText("search");
        back.setVisibility(View.VISIBLE);
        if (movieName.getText() != null && !type.equals("Please select")) {
            movieBiz.getMovie("http://www.imooc.com/api/movie?title="+movieName.getText()+"&types="+type,false);
        }else if(movieName.getText()!=null){
            movieBiz.getMovie("http://www.imooc.com/api/movie?title="+movieName.getText(),false);
        }else if(type.equals("Please select")){
            movieBiz.getMovie("http://www.imooc.com/api/movie?types="+type,false);
        }
    }

    // Back click event
    @OnClick(R.id.back_txt)
    public void backOnClick(){
        tips.setText("It's showing");
        back.setVisibility(View.GONE);
        movieBiz.setUiRecyclerView(movies,true);
    }

MovieDetailsActivity

What is difficult to complete in the details page is the slidable Toolbar control in the upper part, which is realized through CoordinatorLayout + AppBarLayout + CollapsingToolbarLayout
Note: to achieve the sliding effect, you must add a slidable control under AppBarLayout in CollapsingToolbarLayout, such as ListView and RecyclerView. If you want to add a non sliding control such as TextView, you need to set a layer of NestedScrollView on the outside to realize sliding

Detail page layout XML

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appBarLayout"
        android:layout_width="match_parent"
        android:layout_height="300dp">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsingtoolbarlayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:collapsedTitleGravity="center_vertical"
            app:contentScrim="#6200EE"
            app:expandedTitleGravity="bottom|center_horizontal"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:id="@+id/image_details"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="fitXY"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="0.8" />

            <androidx.appcompat.widget.Toolbar
                android:id="@+id/title_details"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin" />
        </com.google.android.material.appbar.CollapsingToolbarLayout>
    </com.google.android.material.appbar.AppBarLayout>

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <TextView
            android:id="@+id/content_details"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="20dp"
            android:textSize="16dp" />
    </androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

Java code
Here, you only need to obtain the information transmitted from the previous interface and display it to the corresponding control

public class MovieDetailsActivity extends AppCompatActivity {

    private static final String TAG = "MovieDetails";
    @BindView(R.id.collapsingtoolbarlayout)
    CollapsingToolbarLayout collapsingToolbarLayout;
    @BindView(R.id.image_details)
    ImageView image;
    @BindView(R.id.title_details)
    androidx.appcompat.widget.Toolbar title;
    @BindView(R.id.content_details)
    TextView content;
    private Movie movie = null;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_details);

        ButterKnife.bind(this);

        // Gets the movie object passed in from the previous interface
        movie = (Movie) getIntent().getSerializableExtra("movie");
        initView(movie);

    }

    private void initView(Movie movie) {
        // Set title color when shrinking
        collapsingToolbarLayout.setCollapsedTitleTextColor(Color.WHITE);
        // Set title color when expanding
        collapsingToolbarLayout.setExpandedTitleColor(Color.WHITE);

        // Display information
        if (movie != null) {
            Glide.with(this)
                    .load(movie.getImageUrl())
                    .into(image);
            title.setTitle(movie.getTitle());
            content.setText("Director:" + movie.getDirectorsName()
                    + "\n to star:" + movie.getCastsName()
                    + "\n Release time:" + movie.getYear()
                    + "\n Type:" + movie.getTypes()
                    + "\n\n Story introduction:\n" + movie.getDescription());
        }
    }
}

Tags: Java Android Design Pattern

Posted by plutoplanet on Tue, 24 May 2022 04:06:34 +0300