news

Senin, 15 Juni 2020

Belajar Aplikasi Android Studi Kasus Proyek Akademi : ViewModel dalam Proyek Academy


Tujuan

Pada Codelab kali ini Anda akan mempelajari bagaimana mengimplementasikan ViewModel dalam proyek Academy. Hasil dari codelab kali ini akan menjadi seperti ini:


20191218113703161173adba65c0c43802822c65f17d2f.gif

Logika Dasar

Membuka Aplikasi → memanggil DataDummy ke ViewModel → mengirim data ke Activity → melakukan perubahan rotasi → data masih terjaga.

Codelab ViewModel

  1. Bukalah proyek Academy yang sudah Anda buat sebelumnya atau Anda bisa unduh di sini.
  2. Lihatlah terlebih dahulu susunan package dan kelas yang sudah ada:
    20191218093730623be083ab749083b7d0476f6387c572.pngAnda akan membuat beberapa kelas ViewModel yang nantinya akan digunakan tiap Fragment atau Activity.
  3. Pertama, buka build.gradle level project dan tambahkan versi untuk library berikut:
    ext {
    ...
    archLifecycleVersion = '2.1.0'
    }
    Setelah itu, buka build.gradle level module: app dan tambahkan library berikut:
    dependencies {
    ...
    //architecture component
    implementation "androidx.lifecycle:lifecycle-viewmodel:$archLifecycleVersion"
    }
  4. Buatlah sebuah kelas baru di package academy dengan nama AcademyViewModel.
    7TmW-FNpmImK1rgSAvsCcbh42d6Jeh2AnLAcSZqtQn6LN0gJmWmJ9SN3JKWOLxGpfNd6_SGXRhR6xrtDIzlTMAcN5KKJrkuOPLmKNawTSYKCoiOE2vlyrars9l91aXA5Zjy4tbOq
    Kemudian pindahkan pemanggilan generateDummyCourse() dari AcademyFragment ke kelas AcademyViewModel:
    Kotlin
    class AcademyViewModel : ViewModel() {

    fun getCourses(): List<CourseEntity> = DataDummy.generateDummyCourses()
    }
    Java
    public class AcademyViewModel extends ViewModel {

    public List<CourseEntity> getCourses() {
    return DataDummy.generateDummyCourses();
    }

    }
  1. Kemudian hubungkan AcademyViewModel dengan AcademyFragment. Bukalah AcademyFragment dan tambahkan kode berikut:
    Kotlin
    class AcademyFragment : Fragment() {
    ...

    override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    if (activity != null) {
    val viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())[AcademyViewModel::class.java]
    val courses = viewModel.getCourses()

    val academyAdapter = AcademyAdapter()
    academyAdapter.setCourses(courses)

    rv_academy.layoutManager = LinearLayoutManager(context)
    rv_academy.setHasFixedSize(true)
    rv_academy.adapter = academyAdapter
    }
    }
    }
    Java
    public class AcademyFragment extends Fragment {
    ...

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    if (getActivity() != null) {
    AcademyViewModel viewModel = new ViewModelProvider(this, new ViewModelProvider.NewInstanceFactory()).get(AcademyViewModel.class);
    List<CourseEntity> courses = viewModel.getCourses();


    AcademyAdapter academyAdapter = new AcademyAdapter();
    academyAdapter.setCourses(courses);

    rvCourse.setLayoutManager(new LinearLayoutManager(getContext()));
    rvCourse.setHasFixedSize(true);
    rvCourse.setAdapter(academyAdapter);
    }
    }
    }
    Dengan begitu, sumber data sudah dipindahkan ke kelas ViewModel. Jalankan Aplikasi Academy, maka hasilnya akan seperti ini:
    201912180953227d294c14169842b541dbc1d651903a66.pngSecara tampilan tidak ada perubahan, karena Anda hanya memindahkan pengambilan data yang sebelumnya dari Activity menjadi ViewModel.
  1. Selanjutnya buatlah kelas ViewModel di package bookmark dan beri nama BookmarkViewModel.
    N4kBwz38aKAcvoAjNR78miJTBWMzhcjqWZLv-yoW8JnYqjTtvalMugC3VeLXoOuAQD5m_Ip4ZSPfjSlXPrA0Th8rIir42kZaUtpVeWRrLO-gBHocBGsAEorTH6OA6QUKbzkOX8TW
    Pindahkan pemanggilan generateDummyCourse() dari BookmarkFragment ke kelas BookmarkViewModel:
    Kotlin
    class BookmarkViewModel : ViewModel() {

    fun getBookmarks(): List<CourseEntity> = DataDummy.generateDummyCourses()
    }
    Java
    public class BookmarkViewModel extends ViewModel {
    List<CourseEntity> getBookmarks() {
    return DataDummy.generateDummyCourses();
    }

    }
  1. Kemudian hubungkan BookmarkAcademy dengan BookmarkFragment. Bukalah BookmarkAcademy dan tambahkan kode berikut:
    Kotlin
    class BookmarkFragment : Fragment(), BookmarkFragmentCallback {
    ...

    override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    if (activity != null) {
    val viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())[BookmarkViewModel::class.java]
    val courses = viewModel.getBookmarks()


    val adapter = BookmarkAdapter(this)
    adapter.setCourses(courses)

    rv_bookmark.layoutManager = LinearLayoutManager(context)
    rv_bookmark.setHasFixedSize(true)
    rv_bookmark.adapter = adapter
    }
    }

    ...
    }
    Java
    public class BookmarkFragment extends Fragment implements BookmarkFragmentCallback {
    ...

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    if (getActivity() != null) {
    BookmarkViewModel viewModel = new ViewModelProvider(this, new ViewModelProvider.NewInstanceFactory()).get(BookmarkViewModel.class);
    List<CourseEntity> courses = viewModel.getBookmarks();


    BookmarkAdapter adapter = new BookmarkAdapter(this);
    adapter.setCourses(courses);

    rvBookmark.setLayoutManager(new LinearLayoutManager(getContext()));
    rvBookmark.setHasFixedSize(true);
    rvBookmark.setAdapter(adapter);
    }
    }

    ...
    }
    Dengan begitu, sumber data sudah dipindahkan ke kelas ViewModel.
  1. Selanjutnya buatlah kelas ViewModel untuk DetailCourseActivity di package detail dan beri nama DetailCourseViewModel.
    t3ub-LoQc-zsvlVnoHAajLLZKzd5SEMzgCs3QiO6INIQWabdvvLL-tOE3XRO4x1fUu5PUzgme3Hn-mpeERSAy53ut7VFhC7iTC3a9ZLIXXA1U_O764nicWh1KQkqLMIye3fV0l9d
    Tambahkan kode pada kelas tersebut untuk menetapkan atau mendapatkan courseId, mendapatkan list module dan mendapatkan CourseEntity.
    Kotlin
    class DetailCourseViewModel : ViewModel() {
    private lateinit var courseId: String

    fun setSelectedCourse(courseId: String) {
    this.courseId = courseId
    }

    fun getCourse(): CourseEntity {
    lateinit var course: CourseEntity
    val coursesEntities = DataDummy.generateDummyCourses()
    for (courseEntity in coursesEntities) {
    if (courseEntity.courseId == courseId) {
    course = courseEntity
    }
    }
    return course
    }

    fun getModules(): List<ModuleEntity> = DataDummy.generateDummyModules(courseId)
    }
    Java
    public class DetailCourseViewModel extends ViewModel {
    private String courseId;

    public void setSelectedCourse(String courseId) {
    this.courseId = courseId;
    }

    public CourseEntity getCourse() {
    CourseEntity course = null;
    ArrayList<CourseEntity> courseEntities = DataDummy.generateDummyCourses();
    for (CourseEntity courseEntity : courseEntities) {
    if (courseEntity.getCourseId().equals(courseId)) {
    course = courseEntity;
    }
    }
    return course;
    }

    public List<ModuleEntity> getModules() {
    return DataDummy.generateDummyModules(courseId);
    }
    }
  1. Selanjutnya ubahlah kode yang ada di DetailCourseActivity untuk menghubungkan DetailCourseViewModel.
    Kotlin
    class DetailCourseActivity : AppCompatActivity() {
    ...

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_detail_course)
    ...

    val viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())[DetailCourseViewModel::class.java]

    val extras = intent.extras
    if (extras != null) {
    val courseId = extras.getString(EXTRA_COURSE)
    if (courseId != null) {
    viewModel.setSelectedCourse(courseId)
    val modules = viewModel.getModules()
    adapter.setModules(modules)
    populateCourse(viewModel.getCourse())
    }
    }

    ...
    }

    ...
    }
    Java
    public class DetailCourseActivity extends AppCompatActivity {
    ...

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

    DetailCourseViewModel viewModel = new ViewModelProvider(this, new ViewModelProvider.NewInstanceFactory()).get(DetailCourseViewModel.class);

    Bundle extras = getIntent().getExtras();
    if (extras != null) {
    String courseId = extras.getString(EXTRA_COURSE);
    if (courseId != null) {
    viewModel.setSelectedCourse(courseId);
    List<ModuleEntity> modules = viewModel.getModules();
    adapter.setModules(modules);
    populateCourse(viewModel.getCourse());

    }
    }

    ...
    }

    ...
    }
    Dengan bantuan kelas ViewModel, courseId akan dipertahankan sampai Activity masuk ke state onDestroy.

  1. Buatlah kembali kelas ViewModel yang akan digunakan untuk CourseReaderActivity. Kemudian beri nama CourseReaderViewModel.
    Nf7nR4S4nZqKu5FkaQ1fedHKVDyRUVXQ41KTHG52Iavdw-mNQuVmB8UXTZUPrT756r6L6irqlasINvD-atWg_2XB-bdiesRLWMhSb7AUr8IYrT8fpX4lAsaTh6Vbs8wzv-vNKC9C
    Setelah itu tambahkanlah kode di kelas tersebut:
    Kotlin
    class CourseReaderViewModel : ViewModel() {

    private lateinit var courseId: String
    private lateinit var moduleId: String

    fun setSelectedCourse(courseId: String) {
    this.courseId = courseId
    }

    fun setSelectedModule(moduleId: String) {
    this.moduleId = moduleId
    }

    fun getModules(): ArrayList<ModuleEntity> = DataDummy.generateDummyModules(courseId)

    fun getSelectedModule(): ModuleEntity {
    lateinit var module: ModuleEntity
    val moduleEntities = getModules()
    for (moduleEntity in moduleEntities) {
    if (moduleEntity.moduleId == moduleId) {
    module = moduleEntity
    module.contentEntity = ContentEntity("<h3 class=\\\"fr-text-bordered\\\">" + module.title + "</h3><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>")
    break
    }
    }
    return module
    }
    }
    Java
    public class CourseReaderViewModel extends ViewModel {

    private String courseId;
    private String moduleId;

    public void setSelectedCourse(String courseId) {
    this.courseId = courseId;
    }

    public void setSelectedModule(String moduleId) {
    this.moduleId = moduleId;
    }

    public ArrayList<ModuleEntity> getModules() {
    return DataDummy.generateDummyModules(courseId);
    }

    public ModuleEntity getSelectedModule() {
    ModuleEntity module = null;
    ArrayList<ModuleEntity> moduleEntities = getModules();
    for (ModuleEntity moduleEntity: moduleEntities) {
    if (moduleEntity.getModuleId().equals(moduleId)) {
    module = moduleEntity;
    module.contentEntity = new ContentEntity("<h3 class=\\\"fr-text-bordered\\\">" + module.getTitle() + "</h3><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>");
    break;
    }
    }
    return module;
    }
    }
    CourseReaderViewModel nantinya juga akan digunakan di ModuleContentFragment dan ModuleListFragment.
  1. Setelah membuat kelas ViewModel, bukalah CourseReaderActivity dan sesuaikanlah menjadi seperti ini:
    Kotlin
    class CourseReaderActivity : AppCompatActivity(), CourseReaderCallback {

    ...

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_course_reader)
    val viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())[CourseReaderViewModel::class.java]

    val bundle = intent.extras
    if (bundle != null) {
    val courseId = bundle.getString(EXTRA_COURSE_ID)
    if (courseId != null) {
    viewModel.setSelectedCourse(courseId)
    populateFragment()
    }
    }
    }
    }
    Java
    public class CourseReaderActivity extends AppCompatActivity implements CourseReaderCallback {
    ...

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

    CourseReaderViewModel viewModel = new ViewModelProvider(this, new ViewModelProvider.NewInstanceFactory()).get(CourseReaderViewModel.class);

    Bundle bundle = getIntent().getExtras();
    if (bundle != null) {
    String courseId = bundle.getString(EXTRA_COURSE_ID);
    if (courseId != null) {
    viewModel.setSelectedCourse(courseId);
    populateFragment();
    }
    }
    }

    ...
    }
  1. Selanjutnya bukalah ModuleListFragment, dan ubah menjadi seperti ini:
    Kotlin
    class ModuleListFragment : Fragment(), MyAdapterClickListener {
    ...

    private lateinit var viewModel: CourseReaderViewModel

    ...

    override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    viewModel = ViewModelProvider(requireActivity(), ViewModelProvider.NewInstanceFactory())[CourseReaderViewModel::class.java]
    adapter = ModuleListAdapter(this)
    populateRecyclerView(viewModel.getModules())

    }

    ...

    override fun onItemClicked(position: Int, moduleId: String) {
    courseReaderCallback.moveTo(position, moduleId)
    viewModel.setSelectedModule(moduleId)
    }

    ...
    }
    Java
    public class ModuleListFragment extends Fragment implements MyAdapterClickListener {

    ...

    private CourseReaderViewModel viewModel;

    ...

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    if (getActivity() != null) {
    viewModel = new ViewModelProvider(requireActivity(), new ViewModelProvider.NewInstanceFactory()).get(CourseReaderViewModel.class);
    adapter = new ModuleListAdapter(this);
    populateRecyclerView(viewModel.getModules());
    }

    }

    ...

    @Override
    public void onItemClicked(int position, String moduleId) {
    courseReaderCallback.moveTo(position, moduleId);
    viewModel.setSelectedModule(moduleId);
    }
    }
    Dengan menambahkan ViewModel di atas, maka sumber data sudah dipindahkan ke ViewModel.
  1. Bukalah ModuleContentFragment dan sesuaikanlah kode pada kelas tersebut menjadi seperti ini:
    Kotlin
    class ModuleContentFragment : Fragment() {

    ...

    override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    if (activity != null) {
    val viewModel = ViewModelProvider(requireActivity(), ViewModelProvider.NewInstanceFactory())[CourseReaderViewModel::class.java]
    val module = viewModel.getSelectedModule()

    populateWebView(module)
    }
    }

    private fun populateWebView(module: ModuleEntity) {
    web_view.loadData(module.contentEntity?.content, "text/html", "UTF-8")
    }
    }
    Java
    public class ModuleContentFragment extends Fragment {
    ...

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    if (getActivity() != null) {
    CourseReaderViewModel viewModel = new ViewModelProvider(requireActivity(), new ViewModelProvider.NewInstanceFactory()).get(CourseReaderViewModel.class);
    ModuleEntity module = viewModel.getSelectedModule();
    populateWebView(module);
    }
    }

    private void populateWebView(ModuleEntity module) {
    webView.loadData(module.contentEntity.getContent(), "text/html", "UTF-8");
    }
    }
    Mengapa ModuleContentFragment bisa langsung tahu ModuleEntity? Jika Anda lihat, tidak ada masukan courseId dan moduleId. Hal ini bisa terjadi karena courseId sudah dimasukkan di CourseReaderActivity dan moduleId dimasukkan di ModuleListFragment. Inilah yang disebut share ViewModel, membagikan ViewModel ke kelas lain. Jadi perlu diperhatikan, pemanggil ViewModel dengan Fragment tersebut:
    Kotlin
    viewModel = ViewModelProvider(requireActivity(), ViewModelProvider.NewInstanceFactory())[CourseReaderViewModel::class.java]
    Java
    viewModel = new ViewModelProvider(requireActivity(), new ViewModelProvider.NewInstanceFactory()).get(CourseReaderViewModel.class);
    Catatan:Jika Anda ganti requireActivity() dengan this, maka Fragment tidak akan mengambil ViewModel dari Activity tetapi akan membuat ViewModel baru.
  1. Langkah terakhir adalah menjalankan aplikasi Anda dan tampilannya akan jadi seperti ini:2019121811345450fe29586c4ebef713ec4a0a075497f6.gifJika dilihat, tidak ada perbedaan tampilan awal karena Anda hanya memindahkan data yang awalnya di Activity menjadi ViewModel. Yang berbeda yaitu tampilan di tiap modul sekarang berbeda-beda sesuai dengan modul yang dipilih, tidak seperti sebelumnya yang sama semua.

Bedah Kode

ViewModel

Perhatikan pemanggilan ViewModel berikut:
AcademyFragment
Kotlin
val viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())[AcademyViewModel::class.java]
Java
AcademyViewModel viewModel = new ViewModelProvider(this, new ViewModelProvider.NewInstanceFactory()).get(AcademyViewModel.class);
DetailCourseActivity
Kotlin
val viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())[DetailCourseViewModel::class.java]
Java
DetailCourseViewModel viewModel = new ViewModelProvider(this, new ViewModelProvider.NewInstanceFactory()).get(DetailCourseViewModel.class);
Untuk pemanggilan ViewModel antara Activity dengan Fragment itu sama. Yang membedakan ketika Fragment akan menggunakan ViewModel yang ada pada Activity (shared ViewModel). Contohnya adalah seperti ini:
ModelContentFragment
Kotlin
val viewModel = ViewModelProvider(requireActivity(), ViewModelProvider.NewInstanceFactory())[CourseReaderViewModel::class.java]
Java
CourseReaderViewModel viewModel = new ViewModelProvider(requireActivity(), new ViewModelProvider.NewInstanceFactory()).get(CourseReaderViewModel.class);
Jadi this diganti dengan requireActivity() untuk menghubungkan Fragment dengan ViewModel yang dipakai di Activity.

Anda bisa unduh proyek Academy tentang ViewModel di sini:
Codelabs selanjutnya akan membahas tentang unit testing dan instrumental testing yang ada pada proyek Academy. Tetap semangat!