Skip to content

Commit

Permalink
L10: Created AnswersActivity, made models Parcelable, handling RV cli…
Browse files Browse the repository at this point in the history
…ck events
  • Loading branch information
AOrobator committed Mar 4, 2019
1 parent 557d259 commit d88beec
Show file tree
Hide file tree
Showing 18 changed files with 383 additions and 31 deletions.
104 changes: 104 additions & 0 deletions lesson10/Lesson10_RecyclerView.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ dependencies {
}
```

## RecyclerView Layout

Now we'll work on our layouts. We'll start off with `list_item_question.xml`, which will be the
layout for each individual list item. You'll want to use a combination of `MaterialCardView`,
`ConstraintLayout`, and a `ChipGroup` inside a `HorizontalScrollView`. Don't worry about making the
Expand Down Expand Up @@ -63,6 +65,8 @@ is the most common LayoutManager you'll use, but there is also, `StaggeredGridLa
item for our layout preview. Note that since this is in the tools namespace, it doesn't affect our
final app at all.

## Loading RecyclerView Data

`QuestionsViewModel` will take in a `QuestionsRepository` defined in `stackoverflow-api` as well as
an instance of `AppSchedulers` so that we can make our network call on a background thread. These
should be passed into the constructor and we'll use a combination of `ViewModelProvider.Factory` and
Expand All @@ -76,6 +80,8 @@ each question to our layout. Once we have this list, we'll call `setValue` on an
`LiveData<List<QuestionViewModel>>`, our `QuestionsActivity` will observe it and pass on the result
to the RecyclerView's adapter.

## Using Adapters

A `RecyclerView` gets its views from a `RecylerView.Adapter`. The adapter lets the `RecyclerView`
know how many views it has and how each list item should be rendered. It's also responsible for
notifying the `RecyclerView` when the underlying data has changed. `RecyclerView.Adapter` has a type
Expand Down Expand Up @@ -137,5 +143,103 @@ The third essential method is `onBindViewHolder`. Once a ViewHolder is created,
it with the relevant information. This method allows us to do just that by passing the position in
the list of the ViewHolder.

## Handling Click Events

It would be rather boring if all you could do was look at the items in a RecyclerView, so let's make
them clickable! In this app, clicking on a question should take you to AnswersActivity where the
answers for that particular question are shown. To start off, we'll modify our list_item so it
responds to touch events. Make the following modifications to the ConstraintLayout directly inside
the MaterialCardView:

```xml
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/clickTarget"
android:background="?selectableItemBackground"
android:clickable="true"
android:focusable="true">
```

We'll give it an id of clickTarget. As an aside, whenever we are referencing an id for the first
time, we need to use `@+id/yourId` instead of `@id/yourId`. For subsequent references, you can use
`@id/yourId`. We then set this view's background to `?selectableItemBackground`. This, in
combination with setting the view as clickable and focusable, will show a touch ripple whenever this
view is touched or clicked.

In QuestionsViewModel, we'll save a reference to the `QuestionsResponse`, and use it in the
following method:

```java
public void onQuestionClicked(int position) {
if (questionsResponse != null) {
Question question = questionsResponse.getItems().get(position);
questionLiveData.setValue(question);
}
}
```

We'll also declare a `MutableLiveData<Question>` and expose it as a `LiveData` so
`QuestionsActivity` can observe which `Question` is clicked. Then we'll pass `QuestionsViewModel` to
our Adapter so each list item/`ViewHolder` can forward click events to it. In `QuestionViewHolder`
add the following statement to the bind method:

```java
binding
.clickTarget
.setOnClickListener(v ->
this.viewModel.onQuestionClicked(getAdapterPosition()));
```

Notice how we never store the adapter position for `QuestionViewHolder`. This is because it is
subject to change when `QuestionViewHolder` gets recycled. Now that we're forwarding click events to
our ViewModel, we'll want to actually react to those changes. In `QuestionsActivity`, observe
`QuestionsViewModel`'s `LiveData<Question>`. When this updates, we'll get an `Intent` with the
`Question` for `AnswersActivity`, then fire the `Intent`:

```java
@Override public void onChanged(Question question) {
Intent intent = AnswersActivity.getIntent(this, question);
startActivity(intent);
}
```

The implementation for getIntent is pretty simple, but there's a little something going on behind
the scenes. We want to add our `Question` to the `Intent` as an extra so it can be retrieved by
`AnswersActivity`. There are many types that `Intent#putExtra` accepts, but `Question` is currently
not one of them. We'll fix this by making both `Question` and `User` implement the `Parcelable`
interface. Implementing the `Parcelable` interface allows us to easily and quickly serialize
objects. This will be much faster than having our model classes implement the`Serializable`
interface. We don't want to write out the implementation by hand, so pressing Option + Enter, after
declaring that you implement the `Parcelable` interface will add the `Parcelable` implementation for
you. This gives us a couple things.

First there is a new constructor that takes in a `Parcel`. When we said `Question` was a
`Parcelable` that means it can be written to a `Parcel`. `Parcel`s generally contain the ability to
read and write primitives as well as other `Parcelable`s. We also get a method
`writeToParcel(Parcel, int)` which does what you think it does. Notice that the data is written and
read from the `Parcel` in the exact same order. This is required for proper functionality.

The next method we see is `describeContents`. This method should always return 0 unless you need to
put a FileDescriptor object into Parcelable. Then you should/must specify CONTENTS_FILE_DESCRIPTOR
as the return value of describeContents()

The last thing we see for the `Parcelable` implementation is a static field
`Creator<Question> CREATOR` that can create a Question from a parcel, as well as create an array of
`Question`s. We'll have to make `User` implement the `Parcelable` interface as well, and we'll
probably want to do that before making `Question` implement the `Parcelable` interface.

Once our model classes implement the `Parcelable` interface, we can retrieve the `Question` in
`onCreate` like so:

```java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_answers);

Question question = getIntent().getParcelableExtra(KEY_QUESTION);
}
```


[StackOverflow]: StackOverflow.jpg "StackOverflow"
[StackOverflow API]: https://api.stackexchange.com/docs
5 changes: 3 additions & 2 deletions lesson10/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@
<uses-permission android:name="android.permission.INTERNET"/>

<application
android:name=".StackOverflowApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:name=".StackOverflowApplication"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name=".view.QuestionsActivity">
<activity android:name=".answers.AnswersActivity"/>
<activity android:name=".questions.view.QuestionsActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.orobator.helloandroid.lesson10.answers;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import com.orobator.helloandroid.lesson10.R;
import com.orobator.helloandroid.stackoverflow.questions.Question;

public class AnswersActivity extends AppCompatActivity {
private static final String KEY_QUESTION = "question";

public static Intent getIntent(Context context, Question question) {
Intent intent = new Intent(context, AnswersActivity.class);
intent.putExtra(KEY_QUESTION, question);
return intent;
}

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

Question question = getIntent().getParcelableExtra(KEY_QUESTION);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.orobator.helloandroid.lesson10.answers.di;

import com.orobator.helloandroid.stackoverflow.answers.AnswersApi;
import com.orobator.helloandroid.stackoverflow.answers.AnswersRepository;
import com.orobator.helloandroid.stackoverflow.answers.AnswersRepositoryImpl;
import dagger.Module;
import dagger.Provides;
import retrofit2.Retrofit;

@Module
public class AnswersActivityModule {
@Provides
public AnswersApi provideAnswersApi(Retrofit retrofit) {
return retrofit.create(AnswersApi.class);
}

@Provides
public AnswersRepository provideAnswersRepository(AnswersApi answersApi) {
return new AnswersRepositoryImpl(answersApi);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package com.orobator.helloandroid.lesson10.di;

import com.orobator.helloandroid.lesson10.view.QuestionsActivity;
import com.orobator.helloandroid.lesson10.answers.AnswersActivity;
import com.orobator.helloandroid.lesson10.answers.di.AnswersActivityModule;
import com.orobator.helloandroid.lesson10.questions.di.QuestionsActivityModule;
import com.orobator.helloandroid.lesson10.questions.view.QuestionsActivity;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;

@Module
public abstract class ActivityBindingModule {
abstract class ActivityBindingModule {
@ContributesAndroidInjector(modules = { QuestionsActivityModule.class })
abstract QuestionsActivity bindNumberFactActivity();
abstract QuestionsActivity bindQuestionsActivity();

@ContributesAndroidInjector(modules = { AnswersActivityModule.class })
abstract AnswersActivity bindAnswersActivity();
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.orobator.helloandroid.lesson10.di;
package com.orobator.helloandroid.lesson10.questions.di;

import com.orobator.helloandroid.common.AppSchedulers;
import com.orobator.helloandroid.lesson10.viewmodel.QuestionsViewModelFactory;
import com.orobator.helloandroid.lesson10.questions.viewmodel.QuestionsViewModelFactory;
import com.orobator.helloandroid.stackoverflow.questions.QuestionsApi;
import com.orobator.helloandroid.stackoverflow.questions.QuestionsDownloader;
import com.orobator.helloandroid.stackoverflow.questions.QuestionsRepository;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package com.orobator.helloandroid.lesson10.view;
package com.orobator.helloandroid.lesson10.questions.view;

import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;
import com.orobator.helloandroid.lesson10.R;
import com.orobator.helloandroid.lesson10.answers.AnswersActivity;
import com.orobator.helloandroid.lesson10.databinding.ActivityQuestionsBinding;
import com.orobator.helloandroid.lesson10.viewmodel.QuestionsViewModel;
import com.orobator.helloandroid.lesson10.viewmodel.QuestionsViewModelFactory;
import com.orobator.helloandroid.lesson10.questions.viewmodel.QuestionsViewModel;
import com.orobator.helloandroid.lesson10.questions.viewmodel.QuestionsViewModelFactory;
import com.orobator.helloandroid.stackoverflow.questions.Question;
import dagger.android.AndroidInjection;
import javax.inject.Inject;

public class QuestionsActivity extends AppCompatActivity {
public class QuestionsActivity extends AppCompatActivity implements Observer<Question> {

@Inject QuestionsViewModelFactory viewModelFactory;

Expand All @@ -29,11 +33,17 @@ protected void onCreate(Bundle savedInstanceState) {

binding.setVm(viewModel);

QuestionsRecyclerAdapter adapter = new QuestionsRecyclerAdapter();
QuestionsRecyclerAdapter adapter = new QuestionsRecyclerAdapter(viewModel);
binding.questionsRecyclerView.setAdapter(adapter);

viewModel.questionViewModelLiveData().observe(this, adapter::updateList);
viewModel.questionViewModelsLiveData().observe(this, adapter::updateList);
viewModel.questionLiveData().observe(this, this);

viewModel.loadQuestions();
}

@Override public void onChanged(Question question) {
Intent intent = AnswersActivity.getIntent(this, question);
startActivity(intent);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.orobator.helloandroid.lesson10.view;
package com.orobator.helloandroid.lesson10.questions.view;

import android.view.LayoutInflater;
import android.view.ViewGroup;
Expand All @@ -8,13 +8,19 @@
import com.google.android.material.chip.Chip;
import com.orobator.helloandroid.lesson10.R;
import com.orobator.helloandroid.lesson10.databinding.ListItemQuestionBinding;
import com.orobator.helloandroid.lesson10.view.QuestionsRecyclerAdapter.QuestionViewHolder;
import com.orobator.helloandroid.lesson10.viewmodel.QuestionViewModel;
import com.orobator.helloandroid.lesson10.questions.view.QuestionsRecyclerAdapter.QuestionViewHolder;
import com.orobator.helloandroid.lesson10.questions.viewmodel.QuestionViewModel;
import com.orobator.helloandroid.lesson10.questions.viewmodel.QuestionsViewModel;
import java.util.Collections;
import java.util.List;

public class QuestionsRecyclerAdapter extends RecyclerView.Adapter<QuestionViewHolder> {
private List<QuestionViewModel> questionViewModels = Collections.emptyList();
private QuestionsViewModel viewModel;

public QuestionsRecyclerAdapter(QuestionsViewModel viewModel) {
this.viewModel = viewModel;
}

@Override public int getItemCount() {
return questionViewModels.size();
Expand All @@ -25,7 +31,7 @@ public QuestionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int view
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
ListItemQuestionBinding binding =
DataBindingUtil.inflate(inflater, R.layout.list_item_question, parent, false);
return new QuestionViewHolder(binding);
return new QuestionViewHolder(binding, viewModel);
}

@Override public void onBindViewHolder(@NonNull QuestionViewHolder holder, int position) {
Expand All @@ -39,11 +45,15 @@ public void updateList(List<QuestionViewModel> questions) {

static public class QuestionViewHolder extends RecyclerView.ViewHolder {
private final ListItemQuestionBinding binding;
private final QuestionsViewModel viewModel;

public QuestionViewHolder(@NonNull ListItemQuestionBinding binding) {
public QuestionViewHolder(
@NonNull ListItemQuestionBinding binding,
QuestionsViewModel viewModel) {
super(binding.getRoot());

this.binding = binding;
this.viewModel = viewModel;
}

public void bind(QuestionViewModel viewModel) {
Expand All @@ -56,6 +66,11 @@ public void bind(QuestionViewModel viewModel) {
chip.setChipBackgroundColorResource(R.color.orange_100);
binding.chipGroup.addView(chip);
}

binding
.clickTarget
.setOnClickListener(v ->
this.viewModel.onQuestionClicked(getAdapterPosition()));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.orobator.helloandroid.lesson10.viewmodel;
package com.orobator.helloandroid.lesson10.questions.viewmodel;

import com.orobator.helloandroid.stackoverflow.questions.Question;
import java.util.List;
Expand Down
Loading

0 comments on commit d88beec

Please sign in to comment.