1 /* 2 * Copyright 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.photopicker.features.photogrid 18 19 import androidx.lifecycle.ViewModel 20 import androidx.lifecycle.viewModelScope 21 import androidx.paging.Pager 22 import androidx.paging.PagingConfig 23 import androidx.paging.cachedIn 24 import com.android.photopicker.core.events.Event 25 import com.android.photopicker.core.events.Events 26 import com.android.photopicker.core.features.FeatureToken.PHOTO_GRID 27 import com.android.photopicker.core.selection.Selection 28 import com.android.photopicker.core.selection.SelectionModifiedResult.FAILURE_SELECTION_LIMIT_EXCEEDED 29 import com.android.photopicker.data.DataService 30 import com.android.photopicker.data.model.Media 31 import com.android.photopicker.extensions.insertMonthSeparators 32 import com.android.photopicker.extensions.toMediaGridItemFromMedia 33 import dagger.hilt.android.lifecycle.HiltViewModel 34 import javax.inject.Inject 35 import kotlinx.coroutines.CoroutineScope 36 import kotlinx.coroutines.launch 37 38 /** 39 * The view model for the primary Photo grid. 40 * 41 * This view model collects the data from [DataService] and caches it in its scope so that loaded 42 * data is saved between navigations so that the composable can maintain list positions when 43 * navigating back and forth between routes. 44 */ 45 @HiltViewModel 46 class PhotoGridViewModel 47 @Inject 48 constructor( 49 private val scopeOverride: CoroutineScope?, 50 private val selection: Selection<Media>, 51 private val dataService: DataService, 52 private val events: Events, 53 ) : ViewModel() { 54 55 // Check if a scope override was injected before using the default [viewModelScope] 56 private val scope: CoroutineScope = 57 if (scopeOverride == null) { 58 this.viewModelScope 59 } else { 60 scopeOverride 61 } 62 63 // Request Media in batches of 50 items 64 private val PHOTO_GRID_PAGE_SIZE = 50 65 66 // The size of the initial load when no pages are loaded. Ensures there is enough content 67 // to cover small scrolls. 68 private val PHOTO_GRID_INITIAL_LOAD_SIZE = PHOTO_GRID_PAGE_SIZE * 3 69 70 // How far from the edge of loaded content before fetching the next page 71 private val PHOTO_GRID_PREFETCH_DISTANCE = PHOTO_GRID_PAGE_SIZE * 2 72 73 // Keep up to 10 pages loaded in memory before unloading pages. 74 private val PHOTO_GRID_MAX_ITEMS_IN_MEMORY = PHOTO_GRID_PAGE_SIZE * 10 75 76 val pagingConfig = 77 PagingConfig( 78 pageSize = PHOTO_GRID_PAGE_SIZE, 79 maxSize = PHOTO_GRID_MAX_ITEMS_IN_MEMORY, 80 initialLoadSize = PHOTO_GRID_INITIAL_LOAD_SIZE, 81 prefetchDistance = PHOTO_GRID_PREFETCH_DISTANCE, 82 ) 83 84 val pager = 85 Pager( 86 PagingConfig(pageSize = PHOTO_GRID_PAGE_SIZE, maxSize = PHOTO_GRID_MAX_ITEMS_IN_MEMORY) <lambda>null87 ) { 88 dataService.mediaPagingSource() 89 } 90 91 /** Export the data from the pager and prepare it for use in the [MediaGrid] */ 92 val data = 93 pager.flow 94 .toMediaGridItemFromMedia() 95 .insertMonthSeparators() 96 // After the load and transformations, cache the data in the viewModelScope. 97 // This ensures that the list position and state will be remembered by the MediaGrid 98 // when navigating back to the PhotoGrid route. 99 .cachedIn(scope) 100 101 /** 102 * Click handler that is called when items in the grid are clicked. Selection updates are made 103 * in the viewModelScope to ensure they aren't canceled if the user navigates away from the 104 * PhotoGrid composable. 105 */ handleGridItemSelectionnull106 fun handleGridItemSelection(item: Media, selectionLimitExceededMessage: String) { 107 scope.launch { 108 val result = selection.toggle(item) 109 if (result == FAILURE_SELECTION_LIMIT_EXCEEDED) { 110 scope.launch { 111 events.dispatch( 112 Event.ShowSnackbarMessage(PHOTO_GRID.token, selectionLimitExceededMessage) 113 ) 114 } 115 } 116 } 117 } 118 } 119