TP MVVM

De $1

Objectifs

Créer une application Android permettant de gérer des articles de presse à partir d'un web service (https://newsapi.org/)

Fonctionnalités

  1. Récupérer une liste d’articles en ligne 
  2. Afficher les articles dans un recycler view 
  3. Afficher le détail d’un article
  4. Sauvegarder les articles dans une BDD local 
  5. Ajouter des fonctionnalités comme (like, partage, etc…)

Architecture de la solution 

create_projetc-11.png

Etape 1: Créer un nouveau module 

Ajouter un nouveau module dans le projet (news). 

Etape 2: Mettre en place les composants nécessaires respectant l'architecture ci-dessus.

Activity principal: HomeActivity 

Fragments: ArticlesFragment, ArticleDetailFragment 

Layouts: home_activity, articles_list_fragment, articlet_detail_fraggment

ViewModels: ArticlesViewModel 

Repository: ArticleRepository 

Data Source: LocalDataSource, RemoteDataSource

 

Activity/Fragment 

  • Modiier l'activité principale pour afficher le fragment ArticlesFragment par défaut. 

Layouts

  1. home_activity.xml doit contenir un FrameLayout dans lequel vous placerez les fragments. 
  2. articles_list_fragment.xml contient un RecyclerView 
  3. articlet_detail_fraggment contient: un Textview pour le titre, un TextView pour la description, une ImageView pour la photo de l'auteur 

Repository 

Modifier le Repository en y ajoutant une méthode permettant de récupérer la liste des articles via le web service 

a. Créer une data class (Article) modélisant les articles 

.....

b. Ajouter les dépendances de Retroit 

//dependances retrofit
implementation "com.squareup.retrofit2:retrofit:2.6.2"
//dependances okhttp
implementation "com.squareup.okhttp3:okhttp:4.2.0"
//dependances gson
implementation 'com.google.code.gson:gson:2.8.5'
//dependances converter gson
implementation "com.squareup.retrofit2:converter-gson:2.5.0" implementation("com.squareup.okhttp3:logging-interceptor:4.3.1")

c. Créer une d'interface pour modéliser les actions du web service (contenant les actions du web service) 

interface ArticleService {
@GET("/articles")
fun getArticles(): Call<List<Article>>
}

d. Créer une instance de Retroit 

Modifier la classe RemoteDataSource 

class RemoteDataSource {
private val service: ArticleService

init {
val httpLoggingInterceptor = HttpLoggingInterceptor()
httpLoggingInterceptor.level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}

val client = OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor)
.readTimeout(30, TimeUnit.SECONDS)
.connectTimeout(30, TimeUnit.SECONDS)
.build()
val retrofit = Retrofit.Builder().apply {
//Ajouter un converter pour JSON
//Ici on utilise gson
addConverterFactory(GsonConverterFactory.create()) client(client)
//Ajouter l'url de base du web service
baseUrl("https://newsapi.org/")
}.build()
//Créer une instance du service
service = retrofit.create(ArticleService::class.java)
}

fun getRemoteArticles(): List<Article> {
val result = service.getArticles().execute()
return if(result.isSuccessful) {
result.body() ?: emptyList()
}else {
emptyList()
}
}
}

 e. Modifier le Repository 

class Repository {
private val online = RemoteDataSource()

fun getArticles(): List<Article> {
return online.getRemoteArticles()
}

}

f. Modifier le ViewModel pour récupérer la liste des articles 

class MyViewModel : ViewModel() {
private val repository: Repository = Repository()
private val _listArticles = MutableLiveData<List<Article>>()
val listArticles: LiveData<List<Article>>
get() = _listArticles

fun loadData() {
val result = repository.getArticles()
_listArticles.value = result
}
}

g. Modifier le fragment ArticlesFragment pour charger la liste des articles et observer les changements sur la liste des articles. 

class ArticlesListFragment: Fragment() {
lateinit var viewModel: MyViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
viewModel.loadData()
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.listArticles.observe(viewLifecycleOwner, Observer {

})
}
}

h. Pour l'instant, afficher la liste des articles dans la console.

Lancer l'application et vérifier que la liste des articles est affichée dans la console.  

Si vous avez suivi à la lettre ce qui est décrit dans le TP, l'application crash au runtime; bon courage pour fixer cette erreur :) 

Kidding, il manque la permission Internet, il suffit de l'ajouter dans le Manifest. 

Recompiler et vérifier que la liste des articles s'affichent dans la console ! 

Oops, ca crash encore ! C'est normal car on lancé les requêtes HTTP sur le thread principal de l'application, ce n'est pas bien de faire ça ! 

Pour l'instant, on va utiliser un thread secondaire; on fera plus proprement dans la prochaine séance !

Modifier la méthode loadData du VM : 

fun loadData() {
object : Thread() {
override fun run() {
val result = repository.getArticles()
_listArticles.value = result
}
}.start()
}

Etape 3: Afficher la liste des articles dans un RecyclerView 

a. Créer un layout modélisant l'affichage des items dans le recycler view 

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

... ...

</androidx.constraintlayout.widget.ConstraintLayout>

b. Créer un Adapter 

class ArticleAdapter : RecyclerView.Adapter<ArticleAdapter.ViewHolder>() {
private val dataset: MutableList<Article> = mutableListOf()

class ViewHolder(val root: View) : RecyclerView.ViewHolder(root) {
fun bind(item: Article) {
val txtTitle = root.findViewById<TextView>(R.id.article_title)
val txtDesc = root.findViewById<TextView>(R.id.article_description)
txtTitle.text = item.title
txtDesc.text = item.description
}
}

fun updateData(list: List<Article>) {
dataset.clear()
dataset.addAll(list)
notifyDataSetChanged()
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val rootView = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item, parent, false)
return ViewHolder(rootView)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(dataset[position])
}

override fun getItemCount() = dataset.size
}

 c. Afficher les données dans le RecyclerView 

..... override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//définir l'orientation des élements (vertical)
recyclerView.layoutManager = LinearLayoutManager(context)
//associer l'adapter à la recyclerview
recyclerView.adapter = adapterRecycler
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.listArticles.observe(viewLifecycleOwner, Observer {
adapterRecycler.updateData(it)
})
}

d. Compiler et tester !

 

Etape 4: Afficher le détail des articles 

 

- Quand l'utilisateur clique sur un item de la liste, afficher la vue de détail en lui passant en paramètre l'article selectionné ! 
 
- Modifier la vue de détail pour afficher les informations de l'article selectionné. 
 
 

Etape 5: On peut faire mieux :) 

Modifier l'interface pour la rendre un peu plus jolie ! 

- Utiliser les cardview pour afficher les items de la liste (https://material.io/develop/android/components/material-card-view/) - aller on va faire comme sur Google news ! 

- Ajouter quelques options (favori, like, share) 

- Enrichir la vue de détail en affichant: l'auteur de l'article (nom + photo), la source, l'image de l'article et un bouton perttant de voir le contenu complet dans un navigateur externe !