Soojeong Lee

2025. 7. 12.

Soojeong Lee

2025. 7. 12.

Soojeong Lee

2025. 7. 12.

상세 페이지 만들어보자

상세 페이지 만들어보자

이젠 상세페이지 차례

상세 페이지 루트 잡기

이제 사용자가 볼 상세 페이지가 필요하다. 주로 목록 -> 상세 흐름으로 앱은 이동하기 때문에 memory_detail.dart 페이지와 라우트를 추가해줬다.

import 'package:go_router/go_router.dart';

import '/screens/memory_detail.dart';
import '/screens/screen2.dart';
import '/screens/screen3.dart';
import 'main_scaffold.dart';
import 'screens/memories.dart';

final goRouter = GoRouter(
  initialLocation: '/memories',
  routes: [
    ShellRoute(
      builder: (context, state, child) => MainScaffold(child: child),
      routes: [
        GoRoute(
          path: '/memories',
          pageBuilder: (context, state) =>
              const NoTransitionPage(child: MemoriesScreen()),
        ),
        GoRoute(
          path: '/screen2',
          pageBuilder: (context, state) =>
              const NoTransitionPage(child: Screen2()),
        ),
        GoRoute(
          path: '/screen3',
          pageBuilder: (context, state) =>
              const NoTransitionPage(child: Screen3()),
        ),
      ],
    ),
    GoRoute(
      path: '/memories/:id',
      builder: (context, state) {
        final id = state.pathParameters['id']!;
        return MemoryDetailScreen(id: id);
      },
    ),
  ],
);

/memories/:id로 별도의 라우트를 만들고 위 Shell Route에 넣지 않은 이유는, MainScaffold에 속하는 레이아웃의 화면이 아니기 때문이다.

다이나믹 루트로 id를 넘기면, 거기에 맞는 상세내역을 가져와 뿌릴거라 pathParameter를 사용했다. 그러면 화면에서 해당 id 정보를 받아 이용할 수 있다.

class MemoryDetailScreen extends StatelessWidget {
  final String id;

  const MemoryDetailScreen({super.key, required this.id});


화면 레이아웃 잡기

아직 객체는 적용하지 않고, 대강 필요한 정보를 뿌릴 형태의 레이아웃을 잡아봤다.

보이고 싶은건

  • 별세한 사람에 대한 짤막한 (별세일, 추모지) 정보

  • 사진/영상을 갤러리 형태로 보이는 것

  • 메모 형태의 텍스트 데이터 또한 미리보기로 갤러리 형태로 보이는 것

이정도로 잡았다.

import 'package:flutter/material.dart';

class MemoryDetailScreen extends StatelessWidget {
  final String id;

  const MemoryDetailScreen({super.key, required this.id});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Memories')),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Card(
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(12),
              ),
              elevation: 2,
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Row(
                  children: [
                    const CircleAvatar(
                      radius: 30,
                      backgroundColor: Colors.grey,
                      // backgroundImage: AssetImage('assets/profile.jpg'),
                    ),
                    const SizedBox(width: 16),
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            'Dad',
                            style: Theme.of(
                              context,
                            ).textTheme.titleLarge?.apply(fontWeightDelta: 2),
                          ),
                          SizedBox(height: 4),
                          Text(
                            'Date of Passing: 02/09/2025',
                            style: TextStyle(color: Colors.grey),
                          ),
                          Text(
                            'Memorial Site: Hambaeksan Memorial Park 9-5-182',
                            style: TextStyle(color: Colors.grey),
                          ),
                        ],
                      ),
                    ),
                    const Icon(Icons.arrow_forward_ios, size: 16),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),
            Text(
              'Photos / Videos',
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 12),
            GridView.builder(
              itemCount: 9,
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 3,
                crossAxisSpacing: 8,
                mainAxisSpacing: 8,
                childAspectRatio: 1,
              ),
              itemBuilder: (context, i) {
                return Container(
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(8),
                    color: Colors.grey[300],
                    // image: DecorationImage(
                    //   image: NetworkImage('https://example.com/image_$i.jpg'),
                    //   fit: BoxFit.cover,
                    // ),
                  ),
                );
              },
            ),
            Text('Texts', style: Theme.of(context).textTheme.titleMedium),
            const SizedBox(height: 12),
            GridView.builder(
              itemCount: 9,
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 3,
                crossAxisSpacing: 8,
                mainAxisSpacing: 8,
                childAspectRatio: 1,
              ),
              itemBuilder: (context, i) {
                return Container(
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(8),
                    color: Colors.grey[300],
                    // image: DecorationImage(
                    //   image: NetworkImage('https://example.com/image_$i.jpg'),
                    //   fit: BoxFit.cover,
                    // ),
                  ),
                );
              },
            ),
            const SizedBox(height: 80),
          ],
        ),
      ),
    );
  }
}

여기서 하나 공유하고 싶은 부분은, 가급적 텍스트의 크기가 굵기를 별도로 매번 설정하지 않고, Theme.of(context).textTheme 클래스에서 가져와 적용하는 것을 추천한다. 유지보수나 디자인 일관성 때문이다.

또 새로운 형태의 데이터를 언제든지 추가할 수 있도록 Floating Action Button까지 추가해줬다.

 floatingActionButton: FloatingActionButton(
  child: const Icon(Icons.add),
  onPressed: () {},
),

이정도로 하면 상세 페이지가 어느정도 나온다.

조금씩 진전이 보인다.

Comments

Comments

Comments