Compreendendo a arquitetura MVVM no Android

Tempo de leitura: 7 minutes

Você conhece a importância da arquitetura e do padrão de design se já trabalhou em projetos de nível intermediário e alto. É essencial manter nosso projeto fracamente acoplado; o que significa manter todos os componentes em nosso projeto separados, o que significa que cada componente tem pouco ou nenhum conhecimento sobre os outros. Isso é especialmente importante em grandes projetos porque as coisas ficam complicadas rapidamente e o resultado final não pode ser mantido. Esses códigos espaguete (fortemente acoplados) também são difíceis de testar porque muitas partes diferentes dependem umas das outras. É por isso que temos padrões de arquitetura para tornar nosso projeto modular, onde cada componente tem uma responsabilidade específica e modificações são possíveis sem modificar outros módulos.

Existem vários padrões arquitetônicos diferentes para escolher, aqui estão alguns dos mais populares sobre os quais você deve ter ouvido falar,

  • MVC
  • MVP
  • MVVM

No Android, podemos usar qualquer um dos itens acima, pois cada um tem suas próprias vantagens e desvantagens (que não vamos discutir aqui). Mas é altamente recomendado pela equipe de desenvolvedores do Google e do Android usar a arquitetura MVVM.

MVVM significa arquitetura Model-View-ViewModel. Existem várias vantagens de usar MVVM em seus projetos, tais como:

  • Torna o projeto vagamente acoplado.
  • Mais fácil de manter.
  • Simples de adicionar um novo recurso ou remover existente.
  • Código muito testável.
  • Isso dá uma ótima estrutura ao seu projeto e torna mais fácil navegar e entender nosso código.

Agora, existem alguns frameworks que podem nos ajudar a implementar o MVVM. Qual você deve usar caso não queira fazer o seu próprio?

O Google fornece sua estrutura composta por vários componentes diferentes que você pode usar e construir para tornar seu trabalho mais simples.

É assim que a implementação da arquitetura MVVM do Google se parece com:

View

Esta parte de nossa arquitetura nos ajuda a construir nossa interface de usuário e a única parte que nossos usuários podem interagir diretamente. Eu consisti em um objeto Fragment definido em “src” e um recurso de layout. Há uma ligação bidirecional entre eles, o que permite fácil compartilhamento de dados.

Aqui está um exemplo, você não precisa entender cada linha, apenas passar por suas implementações.

Fragment :
view/MovieFragment.javapublic class MovieFragment extends Fragment {
    OnPosterClickListener mCallback;
    GridView moviesLayout;
    List<Movie> movies;

    public MovieFragment() {
    }

    public MovieFragment(List<Movie> movies) {
        this.movies = movies;
    }

    public interface OnPosterClickListener {
        void onImageClick(Movie movie);
    }

    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        try {
            mCallback = (OnPosterClickListener) context;
        } catch (ClassCastException e) {
            throw new ClassCastException();
        }
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        final View rootView = inflater.inflate(
            // Inflar fragmento com o layout
                R.layout.fragment_movie_list, container, false);
        moviesLayout = rootView.findViewById(R.id.images_grid_view);
        MovieRecyclerViewAdapter mMovieAdapter = new
                MovieRecyclerViewAdapter(getContext(), movies);

        moviesLayout.setAdapter(mMovieAdapter);
        moviesLayout.setOnItemClickListener((
              (parent, view, position, id) ->
                mCallback.onImageClick(movies.get(position))
        ));
        return rootView;
    }
}

Layout:
res/fragment_movie_list.xml<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"><androidx.recyclerview.widget.RecyclerView
        android:id="@+id/movie_recycler_view"
        android:name="com.example.moviereviewer.MovieFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/toolbar" />

</androidx.constraintlayout.widget.ConstraintLayout>

ViewModel

O objeto ViewModel atua como um intermediário entre View e Model, o que significa que fornece dados para os componentes da IU, como fragmentos ou atividades. Ele também inclui um portador de dados observáveis chamado LiveData que permite ao ViewModel informar ou atualizar a Visualização sempre que os dados são atualizados. É muito importante, principalmente para evitar que nosso aplicativo recarregue com as mudanças de orientação. O que, em última análise, fornece uma ótima experiência do usuário.

Aqui está um exemplo,

// viewmodel/MovieModelView.java
public class MovieViewModel extends AndroidViewModel {

    private static final String DEFAULT_FILTER = "popular";
    private MovieRepository movieRepository;
    private LiveData<TMDB_Response> movieList = 
        new MutableLiveData<>();
    private MutableLiveData<TrailerResponse> trailerList = 
        new MutableLiveData<>();
    private MutableLiveData<ReviewResponse> reviewList = 
        new MutableLiveData<>();
    private MutableLiveData<String> filter =
        new MutableLiveData<>();

    public MovieViewModel(@NonNull Application application) {
        super(application);
        filter.setValue(DEFAULT_FILTER);
        movieRepository = new MovieRepository(application);
    }
    public void setFilter(String value) {
        filter.setValue(value);
    }

    public String getFilter() {
        return filter.getValue();
    }

    public LiveData<MovieResponse> getMovieList(int pageNo) {
        movieList = Transformations.switchMap(filter, 
             (v) -> movieRepository.getMovieList(v, pageNo));
        return movieList;
    }

    public LiveData<TrailerResponse> getTrailerList(int movieId) {
        trailerList = movieRepository.getTrailerList(movieId);
        return trailerList;
    }

    public LiveData<ReviewResponse> getReviewList(int movieId) {
        reviewList = movieRepository.getReviewList(movieId);
        return reviewList;
    }
}

Model

O modelo é responsável por buscar os dados do banco de dados SQLite local ou de um serviço da web. Portanto, ele é dividido em vários componentes.

  • Repositório – é responsável por manipular as informações de dados que incluem onde obter os dados de um serviço da web ou dos modelos de dados persistentes.
public class MovieRepository {
    private MovieDatabaseAPI api;
    private MovieDao movieDao;
    private LiveData<List<Movie>> movieList;

    public MovieRepository(Application application) {
        MovieDatabase movieDatabase = MovieDatabase.getInstance(application);
        movieDao = movieDatabase.movieDao();
        movieList = movieDao.getFavMovies();
        api = RetrofitNetwork.createServie(MovieDatabaseAPI.class);
    }

    public MutableLiveData<TMDB_Response> getMovieList(String filter, int pageNo) {
        final MutableLiveData<TMDB_Response> movies = new MutableLiveData<>();
        api.getMovies(filter, pageNo, Utilities.API_KEY).enqueue(new Callback<TMDB_Response>() {
            @Override
            public void onResponse(@NonNull Call<TMDB_Response> call, @NonNull Response<MovieResponse> response) {
                if (response.isSuccessful()) {
                    movies.setValue(response.body());
                }
            }

            @Override
            public void onFailure(@NonNull Call<TMDB_Response> call, @NonNull Throwable t) {
                movies.setValue(null);
            }
        });
        return movies;
    }

    public MutableLiveData<TrailerResponse> getTrailerList(int movieId) {
        final MutableLiveData<TrailerResponse> trailers = new MutableLiveData<>();
        api.getTrailers(movieId, Utilities.BASE_URL).enqueue(new Callback<TrailerResponse>() {
            @Override
            public void onResponse(@NonNull Call<TrailerResponse> call, @NonNull Response<TrailerResponse> response) {
                if (response.isSuccessful()) {
                    trailers.setValue(response.body());
                }
            }

            @Override
            public void onFailure(@NonNull Call<TrailerResponse> call, @NonNull Throwable t) {
                trailers.setValue(null);
            }
        });
        return trailers;
    }

    public MutableLiveData<ReviewResponse> getReviewList(int movieId) {
        final MutableLiveData<ReviewResponse> reviews = new MutableLiveData<>();
        api.getReviews(movieId, Utilities.API_KEY).enqueue(new Callback<ReviewResponse>() {
            @Override
            public void onResponse(@NonNull Call<ReviewResponse> call, @NonNull Response<ReviewResponse> response) {
                reviews.setValue(response.body());
            }

            @Override public void onFailure(@NonNull Call<ReviewResponse> call, @NonNull Throwable t) { reviews.setValue(null);
            }
        });
        return reviews;
    }

    public LiveData<List<Movie>> getMovies() {
        return movieList;
    }

    public LiveData<List<Movie>> exists(int movieId) {
        return movieDao.getCurrentMovie(movieId);
    }

    public void insert(Movie movie) {
        new InsertMovie(movieDao).execute(movie);
    }

    public void delete(Movie movie) {
        new DeleteMovie(movieDao).execute(movie);
    }

    public static class DeleteMovie extends AsyncTask<Movie, Void, Void> {
        private MovieDao movieDao;

        public DeleteMovie (MovieDao movieDao) {
            this.movieDao = movieDao;
        }

        @Override
        protected Void doInBackground(Movie... movies) {
            movieDao.delete(movies[0]);
            return null;
        }
    }

    public static class InsertMovie extends AsyncTask<Movie, Void, Void> {
        private MovieDao movieDao;

        public InsertMovie (MovieDao movieDao) {
            this.movieDao = movieDao;
        }

        @Override
        protected Void doInBackground(Movie... movies) {
            movieDao.insert(movies[0]);
            return null;
        }
    }
}
  • Room – É um ORM fornecido pelo Google, que fornece uma camada de abstração entre o banco de dados SQLite e nossos dados na forma de objetos. Isso nos dá erros em tempo de compilação, o que é muito melhor do que erros em tempo de execução, que são difíceis de rastrear e depurar.
    Para usar a sala, é muito importante definir nosso esquema. Fazemos isso criando uma classe de modelo de dados e adicionando uma anotação @entity. Também devemos adicionar uma anotação @PrimaryKey ao id da nossa entidade.
@Entity(tableName = "favorites")
public class Movie implements Parcelable {

    @Ignore
    private double popularity;
    private int vote_count;
    private boolean video;
    private String poster_path;
    private int id;
    @Ignore
    private boolean adult;
    @Ignore
    private String backdrop_path;
    @Ignore
    private String original_language;
    @Ignore
    private String original_title;
    private String title;
    private double vote_average;
    private String overview;
    private String release_date;

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeDouble(popularity);
        dest.writeInt(vote_count);
        dest.writeByte((byte) (video ? 1 : 0));
        dest.writeString(poster_path);
        dest.writeInt(id);
        dest.writeByte((byte) (adult ? 1 : 0));
        dest.writeString(backdrop_path);
        dest.writeString(original_language);
        dest.writeString(original_title);
        dest.writeString(title);
        dest.writeDouble(vote_average);
        dest.writeString(overview);
        dest.writeString(release_date);
    }

    protected Movie(Parcel in) {
        popularity = in.readDouble();
        vote_count = in.readInt();
        video = in.readByte() != 0;
        poster_path = in.readString();
        id = in.readInt();
        adult = in.readByte() != 0;
        backdrop_path = in.readString();
        original_language = in.readString();
        original_title = in.readString();
        title = in.readString();
        vote_average = in.readDouble();
        overview = in.readString();
        release_date = in.readString();
    }
    public static final Creator<Movie> CREATOR = new Creator<Movie>() {
        @Override
        public Movie createFromParcel(Parcel in) {
            return new Movie(in);
        }

        @Override
        public Movie[] newArray(int size) {
            return new Movie[size];
        }
    };

    public double getPopularity() {
        return popularity;
    }

    public void setPopularity(double popularity) {
        this.popularity = popularity;
    }

    public int getVote_count() {
        return vote_count;
    }

    public void setVote_count(int vote_count) {
        this.vote_count = vote_count;
    }

    public boolean isVideo() {
        return video;
    }

    public void setVideo(boolean video) {
        this.video = video;
    }

    public String getPoster_path() {
        return poster_path;
    }

    public void setPoster_path(String poster_path) {
        this.poster_path = poster_path;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public boolean isAdult() {
        return adult;
    }

    public void setAdult(boolean adult) {
        this.adult = adult;
    }

    public String getBackdrop_path() {
        return backdrop_path;
    }

    public void setBackdrop_path(String backdrop_path) {
        this.backdrop_path = backdrop_path;
    }

    public String getOriginal_language() {
        return original_language;
    }

    public void setOriginal_language(String original_language) {
        this.original_language = original_language;
    }

    public String getOriginal_title() {
        return original_title;
    }

    public void setOriginal_title(String original_title) {
        this.original_title = original_title;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public double getVote_average() {
        return vote_average;
    }

    public void setVote_average(double vote_average) {
        this.vote_average = vote_average;
    }

    public String getOverview() {
        return overview;
    }

    public void setOverview(String overview) {
        this.overview = overview;
    }

    public String getRelease_date() {
        return release_date;
    }

    public void setRelease_date(String release_date) {
        this.release_date = release_date;
    }

    @Override
    public int describeContents() {
        return 0;
    }
}

Depois disso, devemos criar nossa classe de banco de dados estendendo RoomDatabase, e devemos defini-la como abstrata para que a sala possa cuidar de suas implementações.

@Database(entities = Movie.class, version = 1)
public abstract class MovieDatabase extends RoomDatabase {
    private static MovieDatabase instance;

    public abstract MovieDao movieDao();

    public static synchronized MovieDatabase getInstance(Context context) {
        if (instance == null) {
            instance = Room.databaseBuilder(
                context.getApplicationContext(), 
                MovieDatabase.class, "movie_database"
            ).fallbackToDestructiveMigration()
             .build();
        }
        return instance;
    }
}

Finalmente, defina todas as operações necessárias que exigimos do banco de dados. Para isso, criamos uma interface de acesso ao banco de dados, também conhecida como “objeto de acesso a dados (DAO)”.

@Dao
public interface MovieDao {
    @Insert
    void insert(Movie movie);

    @Query("SELECT * FROM favorites")
    public LiveData<List<Movie>> getFavMovies();

    @Query("SELECT * FROM favorites WHERE id=:movieId")
    public LiveData<List<Movie>> getCurrentMovie(int movieId);

    @Delete
    void delete(Movie movie);
}
  • Web Service – Agora, se quisermos acessar os dados de uma API REST, temos uma biblioteca chamada “Retrofit” à nossa disposição. E nos ajuda a fazer chamadas de rede. Resumindo, ele nos alimenta com a string JSON; convertemos essa resposta JSON em objeto java e adicionamos como uma entidade ao nosso aplicativo.

 

Conclusão

É possível criar aplicativos Android facilmente sem seguir essa arquitetura, mas se quisermos fazer aplicativos que sejam robustos, testáveis, de fácil manutenção e de leitura, devemos usar isso a nosso favor. Escreverei mais sobre a arquitetura MVVM e como seguir as melhores práticas.